diff --git a/priv/resource_snapshots/test_repo/authors/20251018130654_dev.json b/priv/resource_snapshots/test_repo/authors/20251018130654_dev.json new file mode 100644 index 00000000..e14b635e --- /dev/null +++ b/priv/resource_snapshots/test_repo/authors/20251018130654_dev.json @@ -0,0 +1,133 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "precision": null, + "primary_key?": true, + "references": null, + "scale": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "first_name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "last_name", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "bio", + "type": "map" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "bios", + "type": "jsonb" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "badges", + "type": [ + "array", + "text" + ] + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "settings", + "type": "jsonb" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "identities", + "type": "jsonb" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "precision": null, + "primary_key?": false, + "references": null, + "scale": null, + "size": null, + "source": "preferences", + "type": [ + "array", + "map" + ] + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [], + "custom_statements": [], + "has_create_action": true, + "hash": "55CFEDA4CB07DD89D2807B080A6F0E14A0370D7EA0C48E67C36ACAC568137D95", + "identities": [], + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "repo": "Elixir.AshPostgres.TestRepo", + "schema": null, + "table": "authors" +} \ No newline at end of file diff --git a/priv/resource_snapshots/test_repo/authors/20251018130654_dev.json.license b/priv/resource_snapshots/test_repo/authors/20251018130654_dev.json.license new file mode 100644 index 00000000..b0a44fab --- /dev/null +++ b/priv/resource_snapshots/test_repo/authors/20251018130654_dev.json.license @@ -0,0 +1,3 @@ +SPDX-FileCopyrightText: 2019 ash_postgres contributors + +SPDX-License-Identifier: MIT diff --git a/priv/test_repo/migrations/20251018130654_migrate_resources64_dev.exs b/priv/test_repo/migrations/20251018130654_migrate_resources64_dev.exs new file mode 100644 index 00000000..ba9e1baf --- /dev/null +++ b/priv/test_repo/migrations/20251018130654_migrate_resources64_dev.exs @@ -0,0 +1,27 @@ +# SPDX-FileCopyrightText: 2019 ash_postgres contributors +# +# SPDX-License-Identifier: MIT + +defmodule AshPostgres.TestRepo.Migrations.MigrateResources64 do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + alter table(:authors) do + add(:identities, :jsonb) + add(:preferences, {:array, :map}) + end + end + + def down do + alter table(:authors) do + remove(:preferences) + remove(:identities) + end + end +end diff --git a/test/storage_types_test.exs b/test/storage_types_test.exs index ad414bf8..0f818048 100644 --- a/test/storage_types_test.exs +++ b/test/storage_types_test.exs @@ -93,4 +93,86 @@ defmodule AshPostgres.StorageTypesTest do |> Ash.Query.filter(not is_nil(settings["dues_reminders"])) |> Ash.read!() end + + test "can bulk update {:array, CustomTypedStruct} stored as jsonb" do + %{id: id} = + Author + |> Ash.Changeset.for_create( + :create, + %{ + first_name: "Test", + last_name: "User", + identities: [%{provider: "github", uid: "123"}, %{provider: "google", uid: "456"}] + } + ) + |> Ash.create!() + + %BulkResult{records: [author]} = + Author + |> Ash.Query.filter(id == ^id) + |> Ash.bulk_update(:update, %{identities: [%{provider: "gitlab", uid: "789"}]}, + return_errors?: true, + notify?: true, + strategy: [:atomic, :stream, :atomic_batches], + allow_stream_with: :full_read, + return_records?: true + ) + + assert length(author.identities) == 1 + assert %{provider: "gitlab", uid: "789"} = hd(author.identities) + + %BulkResult{records: [author]} = + Author + |> Ash.Query.filter(id == ^id) + |> Ash.bulk_update(:update, %{identities: []}, + return_errors?: true, + notify?: true, + strategy: [:atomic, :stream, :atomic_batches], + allow_stream_with: :full_read, + return_records?: true + ) + + assert author.identities == [] + end + + test "can bulk update {:array, CustomTypedStruct} stored as {:array, :map}" do + %{id: id} = + Author + |> Ash.Changeset.for_create( + :create, + %{ + first_name: "Test", + last_name: "User", + preferences: [%{key: "theme", value: "dark"}, %{key: "lang", value: "en"}] + } + ) + |> Ash.create!() + + %BulkResult{records: [author]} = + Author + |> Ash.Query.filter(id == ^id) + |> Ash.bulk_update(:update, %{preferences: [%{key: "theme", value: "light"}]}, + return_errors?: true, + notify?: true, + strategy: [:atomic, :stream, :atomic_batches], + allow_stream_with: :full_read, + return_records?: true + ) + + assert length(author.preferences) == 1 + assert %{key: "theme", value: "light"} = hd(author.preferences) + + %BulkResult{records: [author]} = + Author + |> Ash.Query.filter(id == ^id) + |> Ash.bulk_update(:update, %{preferences: []}, + return_errors?: true, + notify?: true, + strategy: [:atomic, :stream, :atomic_batches], + allow_stream_with: :full_read, + return_records?: true + ) + + assert author.preferences == [] + end end diff --git a/test/support/resources/author.ex b/test/support/resources/author.ex index 16ca5a8e..9a1203f8 100644 --- a/test/support/resources/author.ex +++ b/test/support/resources/author.ex @@ -23,8 +23,8 @@ defmodule AshPostgres.Test.Author do table("authors") repo(AshPostgres.TestRepo) - migration_types bios: :jsonb, settings: :jsonb - storage_types(bios: :jsonb, settings: :jsonb) + migration_types bios: :jsonb, settings: :jsonb, identities: :jsonb + storage_types(bios: :jsonb, settings: :jsonb, identities: :jsonb) end attributes do @@ -35,6 +35,8 @@ defmodule AshPostgres.Test.Author do attribute(:bios, {:array, :map}, public?: true) attribute(:badges, {:array, :atom}, public?: true) attribute(:settings, AshPostgres.Test.Settings, public?: true) + attribute(:identities, {:array, AshPostgres.Test.Identity}, public?: true) + attribute(:preferences, {:array, AshPostgres.Test.Preference}, public?: true) end actions do diff --git a/test/support/resources/identity.ex b/test/support/resources/identity.ex new file mode 100644 index 00000000..d29aaa07 --- /dev/null +++ b/test/support/resources/identity.ex @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2019 ash_postgres contributors +# +# SPDX-License-Identifier: MIT + +defmodule AshPostgres.Test.Identity do + @moduledoc false + use Ash.TypedStruct + + typed_struct do + field(:provider, :string, allow_nil?: false) + field(:uid, :string, allow_nil?: false) + end +end diff --git a/test/support/resources/preference.ex b/test/support/resources/preference.ex new file mode 100644 index 00000000..b5e707fa --- /dev/null +++ b/test/support/resources/preference.ex @@ -0,0 +1,13 @@ +# SPDX-FileCopyrightText: 2019 ash_postgres contributors +# +# SPDX-License-Identifier: MIT + +defmodule AshPostgres.Test.Preference do + @moduledoc false + use Ash.TypedStruct + + typed_struct do + field(:key, :string, allow_nil?: false) + field(:value, :string, allow_nil?: false) + end +end