From 89c9a6f1c1a64a236db8ce16dfde9e86a2bca448 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CWill?= <“gia19001@byui.edu”> Date: Wed, 11 Feb 2026 13:27:07 -0700 Subject: [PATCH 1/7] issue #611 solution --- .tool-versions | 2 +- CHANGELOG.md | 6 + .../migration_generator.ex | 23 +++- test/migration_generator_test.exs | 121 ++++++++++++++++++ 4 files changed, 149 insertions(+), 3 deletions(-) diff --git a/.tool-versions b/.tool-versions index 0fa52d96..ac3977b1 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ erlang 27.1.2 -elixir 1.18.4-otp-27 +elixir 1.18.1 pipx 1.8.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index f5b1d166..583bb5c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ See [Conventional Commits](https://www.conventionalcommits.org) for commit guide +## [Unreleased] + +### Bug Fixes: + +* avoid dropping and re-adding foreign key when only reference index changes (#611) + ## [v2.6.29](https://github.com/ash-project/ash_postgres/compare/v2.6.28...v2.6.29) (2026-02-03) diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 14c3079e..ee8ecb3c 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -2153,7 +2153,8 @@ defmodule AshPostgres.MigrationGenerator do table: snapshot.table, schema: snapshot.schema, source: attribute.source, - multitenancy: snapshot.multitenancy + multitenancy: snapshot.multitenancy, + old_multitenancy: old_snapshot.multitenancy } end) @@ -2662,7 +2663,8 @@ defmodule AshPostgres.MigrationGenerator do [] end - if Map.get(old_attribute, :references) != Map.get(new_attribute, :references) do + if Map.get(old_attribute, :references) != Map.get(new_attribute, :references) and + references_differ_beyond_index?(old_attribute, new_attribute) do redo_deferrability = if has_reference?(old_snapshot.multitenancy, old_attribute) and differently_deferrable?(new_attribute, old_attribute) do @@ -2770,6 +2772,23 @@ defmodule AshPostgres.MigrationGenerator do defp differently_deferrable?(_, _), do: false + # When the only reference change is index? (add/remove index), we should not emit + # DropForeignKey + AlterAttribute; the separate AddReferenceIndex/RemoveReferenceIndex + # operations handle it. This avoids migrations that drop and re-add the same FK. + defp references_differ_beyond_index?(old_attr, new_attr) do + old_refs = Map.get(old_attr, :references) + new_refs = Map.get(new_attr, :references) + + cond do + old_refs == new_refs -> false + is_nil(old_refs) or is_nil(new_refs) -> true + true -> + old_without_index = Map.delete(old_refs, :index?) + new_without_index = Map.delete(new_refs, :index?) + old_without_index != new_without_index + end + end + # This exists to handle the fact that the remapping of the key name -> source caused attributes # to be considered unequal. We ignore things that only differ in that way using this function. defp attributes_unequal?(left, right, repo, _old_snapshot, _new_snapshot, ignore_names?) do diff --git a/test/migration_generator_test.exs b/test/migration_generator_test.exs index 4345f3e6..1d2dc1db 100644 --- a/test/migration_generator_test.exs +++ b/test/migration_generator_test.exs @@ -1775,6 +1775,127 @@ defmodule AshPostgres.MigrationGeneratorTest do assert File.read!(file) =~ ~S{create index(:posts, [:post_id])} end + test "changing only reference index? does not drop and re-add foreign key (issue #611)" do + # First generate: reference with index?: true + defresource PostRefIdx, "posts" do + attributes do + uuid_primary_key(:id) + attribute(:key_id, :uuid, allow_nil?: false, public?: true) + attribute(:foobar, :string, public?: true) + end + + actions do + defaults([:create, :read, :update, :destroy]) + end + end + + defresource Post2RefIdx, "posts" do + attributes do + uuid_primary_key(:id) + attribute(:name, :string, public?: true) + attribute(:related_key_id, :uuid, public?: true) + end + + relationships do + belongs_to(:post, PostRefIdx, public?: true) + end + + actions do + defaults([:create, :read, :update, :destroy]) + end + + postgres do + references do + reference(:post, index?: true) + end + end + end + + defmodule DomainRefIdx do + use Ash.Domain + resources do + resource(PostRefIdx) + resource(Post2RefIdx) + end + end + + AshPostgres.MigrationGenerator.generate(DomainRefIdx, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false, + auto_name: true + ) + + # Second generate: same reference but index?: false (only index change) + defresource PostRefNoIdx, "posts" do + attributes do + uuid_primary_key(:id) + attribute(:key_id, :uuid, allow_nil?: false, public?: true) + attribute(:foobar, :string, public?: true) + end + + actions do + defaults([:create, :read, :update, :destroy]) + end + end + + defresource Post2RefNoIdx, "posts" do + attributes do + uuid_primary_key(:id) + attribute(:name, :string, public?: true) + attribute(:related_key_id, :uuid, public?: true) + end + + relationships do + belongs_to(:post, PostRefNoIdx, public?: true) + end + + actions do + defaults([:create, :read, :update, :destroy]) + end + + postgres do + references do + reference(:post, index?: false) + end + end + end + + defmodule DomainRefNoIdx do + use Ash.Domain + resources do + resource(PostRefNoIdx) + resource(Post2RefNoIdx) + end + end + + AshPostgres.MigrationGenerator.generate(DomainRefNoIdx, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false, + auto_name: true + ) + + [_, file2] = + Path.wildcard("test_migration_path/**/*_migrate_resources*.exs") + |> Enum.reject(&String.contains?(&1, "extensions")) + |> Enum.sort() + + content = File.read!(file2) + + # Should only drop the index, not touch the foreign key + assert content =~ ~S{drop_if_exists index(:posts, [:post_id])}, + "migration should drop the reference index when index? changes to false" + + refute content =~ ~S{drop constraint(:posts, "posts_post_id_fkey")}, + "migration should not drop the foreign key when only index? changed (issue #611)" + + refute content =~ ~S{modify :post_id, references(}, + "migration should not modify references when only index? changed (issue #611)" + end + test "references with deferrable modifications generate changes with the correct schema" do defposts do attributes do From 6635753aca6ce885257e7016b05c691b67127e15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CWill?= <“gia19001@byui.edu”> Date: Wed, 11 Feb 2026 13:27:07 -0700 Subject: [PATCH 2/7] issue #611 solution --- .tool-versions | 2 +- .../migration_generator.ex | 23 +++- test/migration_generator_test.exs | 121 ++++++++++++++++++ 3 files changed, 143 insertions(+), 3 deletions(-) diff --git a/.tool-versions b/.tool-versions index 0fa52d96..ac3977b1 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ erlang 27.1.2 -elixir 1.18.4-otp-27 +elixir 1.18.1 pipx 1.8.0 diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 14c3079e..ee8ecb3c 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -2153,7 +2153,8 @@ defmodule AshPostgres.MigrationGenerator do table: snapshot.table, schema: snapshot.schema, source: attribute.source, - multitenancy: snapshot.multitenancy + multitenancy: snapshot.multitenancy, + old_multitenancy: old_snapshot.multitenancy } end) @@ -2662,7 +2663,8 @@ defmodule AshPostgres.MigrationGenerator do [] end - if Map.get(old_attribute, :references) != Map.get(new_attribute, :references) do + if Map.get(old_attribute, :references) != Map.get(new_attribute, :references) and + references_differ_beyond_index?(old_attribute, new_attribute) do redo_deferrability = if has_reference?(old_snapshot.multitenancy, old_attribute) and differently_deferrable?(new_attribute, old_attribute) do @@ -2770,6 +2772,23 @@ defmodule AshPostgres.MigrationGenerator do defp differently_deferrable?(_, _), do: false + # When the only reference change is index? (add/remove index), we should not emit + # DropForeignKey + AlterAttribute; the separate AddReferenceIndex/RemoveReferenceIndex + # operations handle it. This avoids migrations that drop and re-add the same FK. + defp references_differ_beyond_index?(old_attr, new_attr) do + old_refs = Map.get(old_attr, :references) + new_refs = Map.get(new_attr, :references) + + cond do + old_refs == new_refs -> false + is_nil(old_refs) or is_nil(new_refs) -> true + true -> + old_without_index = Map.delete(old_refs, :index?) + new_without_index = Map.delete(new_refs, :index?) + old_without_index != new_without_index + end + end + # This exists to handle the fact that the remapping of the key name -> source caused attributes # to be considered unequal. We ignore things that only differ in that way using this function. defp attributes_unequal?(left, right, repo, _old_snapshot, _new_snapshot, ignore_names?) do diff --git a/test/migration_generator_test.exs b/test/migration_generator_test.exs index 4345f3e6..1d2dc1db 100644 --- a/test/migration_generator_test.exs +++ b/test/migration_generator_test.exs @@ -1775,6 +1775,127 @@ defmodule AshPostgres.MigrationGeneratorTest do assert File.read!(file) =~ ~S{create index(:posts, [:post_id])} end + test "changing only reference index? does not drop and re-add foreign key (issue #611)" do + # First generate: reference with index?: true + defresource PostRefIdx, "posts" do + attributes do + uuid_primary_key(:id) + attribute(:key_id, :uuid, allow_nil?: false, public?: true) + attribute(:foobar, :string, public?: true) + end + + actions do + defaults([:create, :read, :update, :destroy]) + end + end + + defresource Post2RefIdx, "posts" do + attributes do + uuid_primary_key(:id) + attribute(:name, :string, public?: true) + attribute(:related_key_id, :uuid, public?: true) + end + + relationships do + belongs_to(:post, PostRefIdx, public?: true) + end + + actions do + defaults([:create, :read, :update, :destroy]) + end + + postgres do + references do + reference(:post, index?: true) + end + end + end + + defmodule DomainRefIdx do + use Ash.Domain + resources do + resource(PostRefIdx) + resource(Post2RefIdx) + end + end + + AshPostgres.MigrationGenerator.generate(DomainRefIdx, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false, + auto_name: true + ) + + # Second generate: same reference but index?: false (only index change) + defresource PostRefNoIdx, "posts" do + attributes do + uuid_primary_key(:id) + attribute(:key_id, :uuid, allow_nil?: false, public?: true) + attribute(:foobar, :string, public?: true) + end + + actions do + defaults([:create, :read, :update, :destroy]) + end + end + + defresource Post2RefNoIdx, "posts" do + attributes do + uuid_primary_key(:id) + attribute(:name, :string, public?: true) + attribute(:related_key_id, :uuid, public?: true) + end + + relationships do + belongs_to(:post, PostRefNoIdx, public?: true) + end + + actions do + defaults([:create, :read, :update, :destroy]) + end + + postgres do + references do + reference(:post, index?: false) + end + end + end + + defmodule DomainRefNoIdx do + use Ash.Domain + resources do + resource(PostRefNoIdx) + resource(Post2RefNoIdx) + end + end + + AshPostgres.MigrationGenerator.generate(DomainRefNoIdx, + snapshot_path: "test_snapshots_path", + migration_path: "test_migration_path", + quiet: true, + format: false, + auto_name: true + ) + + [_, file2] = + Path.wildcard("test_migration_path/**/*_migrate_resources*.exs") + |> Enum.reject(&String.contains?(&1, "extensions")) + |> Enum.sort() + + content = File.read!(file2) + + # Should only drop the index, not touch the foreign key + assert content =~ ~S{drop_if_exists index(:posts, [:post_id])}, + "migration should drop the reference index when index? changes to false" + + refute content =~ ~S{drop constraint(:posts, "posts_post_id_fkey")}, + "migration should not drop the foreign key when only index? changed (issue #611)" + + refute content =~ ~S{modify :post_id, references(}, + "migration should not modify references when only index? changed (issue #611)" + end + test "references with deferrable modifications generate changes with the correct schema" do defposts do attributes do From 76e3ca145f050e7cda1ff6e3c66bd8b994a4493c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CWill?= <“gia19001@byui.edu”> Date: Wed, 18 Feb 2026 13:09:27 -0700 Subject: [PATCH 3/7] Revert CHANGELOG and .tool-versions per maintainer feedback --- .tool-versions | 2 +- CHANGELOG.md | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/.tool-versions b/.tool-versions index ac3977b1..0fa52d96 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ erlang 27.1.2 -elixir 1.18.1 +elixir 1.18.4-otp-27 pipx 1.8.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index f5b1d166..66cea698 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,35 @@ See [Conventional Commits](https://www.conventionalcommits.org) for commit guide +## [v2.6.32](https://github.com/ash-project/ash_postgres/compare/v2.6.31...v2.6.32) (2026-02-11) + + + + +### Bug Fixes: + +* produce correct error on `restrict` managed_relationship behavior (#690) by lincolnhuls + +### Improvements: + +* Split up usage rules into sub-rules (#691) by Mylan Connolly + +## [v2.6.31](https://github.com/ash-project/ash_postgres/compare/v2.6.30...v2.6.31) (2026-02-05) + + + + +### Bug Fixes: + +* handle atomic no_rollback errors during creates by Zach Daniel + +* correct test expectation for `first` aggregate with `include_nil?` (#683) by sevenseacat + +## [v2.6.30](https://github.com/ash-project/ash_postgres/compare/v2.6.29...v2.6.30) (2026-02-04) + + +* fix for atomic create support + ## [v2.6.29](https://github.com/ash-project/ash_postgres/compare/v2.6.28...v2.6.29) (2026-02-03) From c00ef1779eca023dbb5c0c2134e04d08907262ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CWill?= <“gia19001@byui.edu”> Date: Wed, 18 Feb 2026 13:55:09 -0700 Subject: [PATCH 4/7] Update #611 test to use setup context (snapshot_path, migration_path) --- test/migration_generator_test.exs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test/migration_generator_test.exs b/test/migration_generator_test.exs index 6452c0f0..2f6a7368 100644 --- a/test/migration_generator_test.exs +++ b/test/migration_generator_test.exs @@ -1817,7 +1817,10 @@ defmodule AshPostgres.MigrationGeneratorTest do assert File.read!(file) =~ ~S{create index(:posts, [:post_id])} end - test "changing only reference index? does not drop and re-add foreign key (issue #611)" do + test "changing only reference index? does not drop and re-add foreign key (issue #611)", %{ + snapshot_path: snapshot_path, + migration_path: migration_path + } do # First generate: reference with index?: true defresource PostRefIdx, "posts" do attributes do @@ -1862,8 +1865,8 @@ defmodule AshPostgres.MigrationGeneratorTest do end AshPostgres.MigrationGenerator.generate(DomainRefIdx, - snapshot_path: "test_snapshots_path", - migration_path: "test_migration_path", + snapshot_path: snapshot_path, + migration_path: migration_path, quiet: true, format: false, auto_name: true @@ -1913,15 +1916,15 @@ defmodule AshPostgres.MigrationGeneratorTest do end AshPostgres.MigrationGenerator.generate(DomainRefNoIdx, - snapshot_path: "test_snapshots_path", - migration_path: "test_migration_path", + snapshot_path: snapshot_path, + migration_path: migration_path, quiet: true, format: false, auto_name: true ) [_, file2] = - Path.wildcard("test_migration_path/**/*_migrate_resources*.exs") + Path.wildcard("#{migration_path}/**/*_migrate_resources*.exs") |> Enum.reject(&String.contains?(&1, "extensions")) |> Enum.sort() From 7765d59b1e87dfe08817dee933f90b59449ee6b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CWill?= <“gia19001@byui.edu”> Date: Mon, 2 Mar 2026 14:08:31 -0700 Subject: [PATCH 5/7] Ensure identity unique indexes precede self-referential foreign keys --- .../migration_generator.ex | 84 ++++++++++++++++++- lib/migration_generator/operation.ex | 1 + test/migration_generator_test.exs | 79 +++++++++++++++++ 3 files changed, 162 insertions(+), 2 deletions(-) diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 1d0af41b..d1bb0e2d 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -1518,6 +1518,76 @@ defmodule AshPostgres.MigrationGenerator do true end + # CreateTable must appear before AddUniqueIndex for the same table (table must exist first). + defp after?( + %Operation.CreateTable{table: table, schema: schema}, + %Operation.AddUniqueIndex{table: table, schema: schema} + ), + do: false + + # Unique index must be created before any alter that adds FKs referencing it (issue #236). + defp after?( + %Operation.AddUniqueIndex{table: table, schema: schema}, + %Operation.AlterAttribute{table: table, schema: schema} + ), + do: false + + # Do not place AddUniqueIndex after CreateTable (must be after columns are added). + defp after?( + %Operation.AddUniqueIndex{table: table, schema: schema}, + %Operation.CreateTable{table: table, schema: schema} + ), + do: false + + # Place AddUniqueIndex after the last AddAttribute for the same table so it + # appears before AlterAttributes (issue #236). + defp after?( + %Operation.AddUniqueIndex{ + insert_after_attribute_order: max_order, + table: table, + schema: schema + }, + %Operation.AddAttribute{ + table: table, + schema: schema, + attribute: %{order: order} + } + ) + when not is_nil(max_order) and order == max_order, + do: true + + defp after?( + %Operation.AddUniqueIndex{ + insert_after_attribute_order: max_order, + table: table, + schema: schema + }, + %Operation.AddAttribute{ + table: table, + schema: schema, + attribute: %{order: order} + } + ) + when not is_nil(max_order) and order != max_order, + do: false + + defp after?( + %Operation.AddUniqueIndex{ + identity: %{keys: keys}, + table: table, + schema: schema + }, + %Operation.AlterAttribute{ + table: table, + schema: schema, + new_attribute: %{ + references: %{table: table, destination_attribute: destination_attribute} + } + } + ) do + destination_attribute not in List.wrap(keys) + end + defp after?( %Operation.AddUniqueIndex{ table: table, @@ -2267,6 +2337,7 @@ defmodule AshPostgres.MigrationGenerator do identity: identity, schema: snapshot.schema, table: snapshot.table, + insert_after_attribute_order: max(0, length(snapshot.attributes) - 1), concurrently: opts.concurrent_indexes } end) @@ -2301,13 +2372,22 @@ defmodule AshPostgres.MigrationGenerator do } end) + # Place unique indexes after create/add attributes but before alter attributes + # so FKs that reference identity columns see the unique index (issue #236). + {creates_and_adds, alter_and_rest} = + Enum.split_while(attribute_operations, fn + %Operation.AlterAttribute{} -> false + _ -> true + end) + [ pkey_operations, unique_indexes_to_remove, - attribute_operations, + creates_and_adds, + unique_indexes_to_add, + alter_and_rest, reference_indexes_to_add, reference_indexes_to_remove, - unique_indexes_to_add, unique_indexes_to_rename, constraints_to_remove, constraints_to_add, diff --git a/lib/migration_generator/operation.ex b/lib/migration_generator/operation.ex index 978298d3..405baba7 100644 --- a/lib/migration_generator/operation.ex +++ b/lib/migration_generator/operation.ex @@ -862,6 +862,7 @@ defmodule AshPostgres.MigrationGenerator.Operation do :schema, :multitenancy, :old_multitenancy, + :insert_after_attribute_order, no_phase: true, concurrently: false ] diff --git a/test/migration_generator_test.exs b/test/migration_generator_test.exs index 2f6a7368..43cc11f0 100644 --- a/test/migration_generator_test.exs +++ b/test/migration_generator_test.exs @@ -99,6 +99,13 @@ defmodule AshPostgres.MigrationGeneratorTest do end end + defp position_of_substring(string, substring) do + case :binary.match(string, substring) do + {pos, _len} -> pos + :nomatch -> nil + end + end + defmacrop defresource(mod, table, do: body) do quote do Code.compiler_options(ignore_module_conflict: true) @@ -1677,6 +1684,78 @@ defmodule AshPostgres.MigrationGeneratorTest do ~S[references(:posts, column: :id, name: "posts_post_id_fkey", type: :uuid, prefix: "public")] end + @tag :issue_236 + test "unique index is created before dependent foreign key (issue #236)", %{ + snapshot_path: snapshot_path, + migration_path: migration_path + } do + defresource Template, "templates" do + attributes do + uuid_primary_key(:id) + end + end + + defresource Phase, "phases" do + attributes do + uuid_primary_key(:id) + end + end + + defresource TemplatePhase, "template_phase" do + attributes do + uuid_primary_key(:id) + attribute(:name, :string, allow_nil?: false, public?: true) + end + + identities do + identity(:id, [:id]) + end + + relationships do + belongs_to(:template, Template, primary_key?: true, allow_nil?: false, public?: true) + belongs_to(:phase, Phase, primary_key?: true, allow_nil?: false, public?: true) + + belongs_to(:template_phase, __MODULE__) do + source_attribute(:follows) + destination_attribute(:id) + attribute_writable?(true) + allow_nil?(true) + public?(true) + end + end + end + + defdomain([Template, Phase, TemplatePhase]) + + AshPostgres.MigrationGenerator.generate(Domain, + snapshot_path: snapshot_path, + migration_path: migration_path, + quiet: true, + format: false, + auto_name: true + ) + + assert [file] = + Path.wildcard("#{migration_path}/**/*_migrate_resources*.exs") + |> Enum.reject(&String.contains?(&1, "extensions")) + + file_contents = File.read!(file) + + unique_index_pos = + position_of_substring( + file_contents, + ~S{create unique_index(:template_phase, [:id], name: "template_phase_id_index")} + ) + + follows_fk_pos = position_of_substring(file_contents, "references(:template_phase") + + assert unique_index_pos && follows_fk_pos, + "expected migration to contain both the unique index and the follows foreign key" + + assert unique_index_pos < follows_fk_pos, + "expected unique index creation to appear before the follows foreign key modification" + end + test "references are inferred automatically if the attribute has a different type", %{ snapshot_path: snapshot_path, migration_path: migration_path From 48c2be5193b4f9572c85391c0d7594493173c4f6 Mon Sep 17 00:00:00 2001 From: Zach Daniel Date: Tue, 3 Mar 2026 06:24:15 -0500 Subject: [PATCH 6/7] Apply suggestion from @zachdaniel --- lib/migration_generator/migration_generator.ex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 9208bdf8..4eca73b7 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -1525,7 +1525,7 @@ defmodule AshPostgres.MigrationGenerator do ), do: false - # Unique index must be created before any alter that adds FKs referencing it (issue #236). + # Unique index must be created before any alter that adds FKs referencing it. defp after?( %Operation.AddUniqueIndex{table: table, schema: schema}, %Operation.AlterAttribute{table: table, schema: schema} From ec7988077b4b86125fa66d385624e24928b7d96f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=9CWill?= <“gia19001@byui.edu”> Date: Wed, 4 Mar 2026 13:23:31 -0700 Subject: [PATCH 7/7] Compute AddUniqueIndex order from identity keys --- lib/migration_generator/migration_generator.ex | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/migration_generator/migration_generator.ex b/lib/migration_generator/migration_generator.ex index 4eca73b7..bbd9b573 100644 --- a/lib/migration_generator/migration_generator.ex +++ b/lib/migration_generator/migration_generator.ex @@ -2333,11 +2333,22 @@ defmodule AshPostgres.MigrationGenerator do end) end |> Enum.map(fn identity -> + orders = + identity.keys + |> Enum.map(&Enum.find_index(snapshot.attributes, fn attr -> attr.source == &1 end)) + |> Enum.reject(&is_nil/1) + + insert_after_attribute_order = + case orders do + [] -> nil + _ -> Enum.max(orders) + end + %Operation.AddUniqueIndex{ identity: identity, schema: snapshot.schema, table: snapshot.table, - insert_after_attribute_order: max(0, length(snapshot.attributes) - 1), + insert_after_attribute_order: insert_after_attribute_order, concurrently: opts.concurrent_indexes } end)