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
19 changes: 19 additions & 0 deletions lib/migration_generator/operation_deps.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ defmodule AshPostgres.MigrationGenerator.OperationDeps do
barrier would need it to run last — contradictory. Give each operation
type the ordering it needs via `requires/1` instead.

Operations flagged `no_phase: true` float freely (they skip the create/alter
phases; see `group_into_phases/3`) unless `requires/1` gives them an edge. One
whose `up` SQL names an object another operation creates in the same batch must
require that object's fact, or it runs first. Only `AlterDeferrability` and
`AddPrimaryKey` do; the rest act on pre-existing objects, render nothing in
`up`, or are ordered by their facts' consumers.

Requiring a fact waits on *every* operation that provides it, not just one
— see `toposort_operations/1`'s `provides_index`. That's what makes
`:table_structure_ready` works as a catch-all: many operation types provide
Expand Down Expand Up @@ -330,6 +337,18 @@ defmodule AshPostgres.MigrationGenerator.OperationDeps do
[{:table_ready, key(table, schema)}] ++
reference_requirements(attribute, table, schema)

# `ALTER CONSTRAINT ... DEFERRABLE` runs against a foreign key an `AddAttribute`
# in this batch creates (it provides `table_columns_settled`); without this it
# floats ahead and fails "constraint does not exist". `:down` drops it early_tier.
%Operation.AlterDeferrability{table: table, schema: schema, direction: :up} ->
[{:table_columns_settled, key(table, schema)}]

# `ADD PRIMARY KEY (keys)` runs against columns an `AddAttribute` in this batch can
# add (a new attribute folded into a composite key), so require each; without this
# it floats ahead of the add. Vacuous for pre-existing keys, and no cycle.
%Operation.AddPrimaryKey{table: table, schema: schema, keys: keys} ->
Enum.map(keys, &{:column_ready, key(table, schema, &1)})

%Operation.AlterAttribute{
table: table,
schema: schema,
Expand Down
92 changes: 92 additions & 0 deletions test/migration_generator/operation_deps_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -809,4 +809,96 @@ defmodule AshPostgres.MigrationGenerator.OperationDepsTest do
assert fact in OperationDeps.requires(remove_index)
end
end

describe "foreign key deferrability" do
test "AlterDeferrability{direction: :up} requires table_columns_settled, satisfied by the AddAttribute that adds the foreign key it alters" do
alter_def = %Operation.AlterDeferrability{
table: "comments",
schema: nil,
references: %{},
direction: :up
}

add_fk_column = %Operation.AddAttribute{
table: "comments",
schema: nil,
attribute: %{
source: :post_id,
primary_key?: false,
references: %{table: "posts", destination_attribute: :id, schema: "public"}
}
}

[settled_fact] =
OperationDeps.provides(add_fk_column)
|> Enum.filter(&match?({:table_columns_settled, _}, &1))

assert settled_fact == {:table_columns_settled, {"public", "comments"}}
assert settled_fact in OperationDeps.requires(alter_def)
end

test "AlterDeferrability{direction: :up} is ordered after the AddAttribute that creates the foreign key" do
add_fk_column = %Operation.AddAttribute{
table: "comments",
schema: nil,
attribute: %{
source: :post_id,
primary_key?: false,
references: %{table: "posts", destination_attribute: :id, schema: "public"}
}
}

alter_def = %Operation.AlterDeferrability{
table: "comments",
schema: nil,
references: %{},
direction: :up
}

# Alter listed first on purpose: only the dependency edge can reorder it.
operations = MigrationGenerator.toposort_operations([alter_def, add_fk_column])

add_index = Enum.find_index(operations, &match?(%Operation.AddAttribute{}, &1))
def_index = Enum.find_index(operations, &match?(%Operation.AlterDeferrability{}, &1))

assert add_index < def_index
end
end

describe "primary key columns" do
test "AddPrimaryKey requires column_ready for each key, satisfied by the AddAttribute that adds a new composite-pkey column" do
# Widening the primary key to include a newly added column (cell_id).
add_pk = %Operation.AddPrimaryKey{table: "accounts", schema: nil, keys: [:id, :cell_id]}

add_cell_id = %Operation.AddAttribute{
table: "accounts",
schema: nil,
attribute: %{source: :cell_id, primary_key?: true}
}

[column_ready_fact] =
OperationDeps.provides(add_cell_id) |> Enum.filter(&match?({:column_ready, _}, &1))

assert column_ready_fact == {:column_ready, {"public", "accounts", :cell_id}}
assert column_ready_fact in OperationDeps.requires(add_pk)
end

test "AddPrimaryKey is ordered after the AddAttribute that creates a new composite primary-key column" do
add_cell_id = %Operation.AddAttribute{
table: "accounts",
schema: nil,
attribute: %{source: :cell_id, primary_key?: true}
}

add_pk = %Operation.AddPrimaryKey{table: "accounts", schema: nil, keys: [:id, :cell_id]}

# AddPrimaryKey listed first on purpose: only the dependency edge can reorder it.
operations = MigrationGenerator.toposort_operations([add_pk, add_cell_id])

add_index = Enum.find_index(operations, &match?(%Operation.AddAttribute{}, &1))
pk_index = Enum.find_index(operations, &match?(%Operation.AddPrimaryKey{}, &1))

assert add_index < pk_index
end
end
end