Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
erlang 27.1.2
elixir 1.18.4-otp-27
elixir 1.18.1
pipx 1.8.0
23 changes: 21 additions & 2 deletions lib/migration_generator/migration_generator.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2154,7 +2154,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)

Expand Down Expand Up @@ -2663,7 +2664,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
Expand Down Expand Up @@ -2771,6 +2773,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
Expand Down
124 changes: 124 additions & 0 deletions test/migration_generator_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1817,6 +1817,130 @@ 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)", %{
snapshot_path: snapshot_path,
migration_path: migration_path
} 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: snapshot_path,
migration_path: 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: snapshot_path,
migration_path: migration_path,
quiet: true,
format: false,
auto_name: true
)

[_, file2] =
Path.wildcard("#{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", %{
snapshot_path: snapshot_path,
migration_path: migration_path
Expand Down
Loading