Computes the dependency graph used to order migration operations.
Each operation may provide facts (things that become true once it runs)
and require facts (things that must already be true before it can run).
AshPostgres.MigrationGenerator.toposort_operations/1
turns these into a dependency graph and topologically sorts it.
There is deliberately no symmetric "late tier" counterpart to
early_tier?/1 (a global "runs after everything else" barrier). Adding one
creates a real cycle: anything that requires a same-table fact regardless
of provider (e.g. AddCustomStatement requiring its own table's structure
to be ready) would need that "late" op to run first, while the "late" op's
barrier would need it to run last — contradictory. Give each operation
type the ordering it needs via requires/1 instead.
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
it, so an op that requires it (e.g.
AddCustomStatement's own-table or after_tables requirement)
transparently waits for all of that table's work, without needing to
enumerate every attribute/index/constraint by hand.
Requiring a fact that nothing in the current batch provides is vacuously
satisfied — it adds no dependency edge at all, rather than blocking or
raising. This is intentional: e.g. reference_requirements/1 requires a
referenced table/column to be :column_ready, but if that table already
exists from an earlier migration (so nothing in this batch provides the
fact), the requirement is trivially met and doesn't hold anything up.
A generated migration's down isn't independently derived — it's built by
walking the same operation order in reverse and rendering each operation's
down/1 (see MigrationGenerator.build_up_and_down/1). So an ordering
that's harmless for up can still be wrong: if RenameAttribute isn't
required to run after a RemoveCustomIndex on the same table, down ends
up recreating that index (RemoveCustomIndex.down) before undoing the
rename, referencing a column name that doesn't exist yet at that point.
Postgres itself doesn't need indexes/constraints removed before a rename or
a column drop — it tracks index predicates, expression indexes, and
same-table constraints internally by column position and updates or drops
them automatically (verified directly: a partial unique index's WHERE
clause and a custom index's expression are both rewritten after RENAME COLUMN; a CHECK constraint on a dropped column is dropped with it, no
CASCADE needed). But several facts below exist anyway, purely so the
reversed down sequence stays valid. Only genuinely cross-table
dependencies (a foreign key on another table) block a Postgres statement
directly, which is what :column_fk_dropped is for.
Facts
Each fact is a 2-tuple {name, key}. key is {schema, table} for a
table-scoped fact, or {schema, table, x} for a fact scoped to some x
within that table — see key/2, key/3. That third element is a column
for every fact below except :custom_index_removed (an index name) and
:unique_index_created (a sorted column list); facts are named
table_*/column_*/index_* to match what their key actually scopes
over, not just its shape.
Table-scoped (key = {schema, table}):
These first four track "how done is this table", but not as one single
chain — :table_ready is provided by a disjoint set of operations
(CreateTable/RenameTable/MoveTableSchema) from :table_columns_settled
(AddAttribute/RenameAttribute/AlterAttribute/RemoveAttribute), so
requiring both together is not redundant — neither subsumes the other.
Both converge at :table_structure_ready, which every structural operation
provides:
:table_ready— the table exists (CreateTable/RenameTable/MoveTableSchema).:table_columns_settled— every attribute add/alter/rename/remove for this table has already run — a conservative margin for consumers whose raw SQL (a filtered unique index'swhere, a custom index's expression, a check constraint'scheck:) might reference a column that's about to be added, altered, renamed, or removed, and can't be parsed to know which columns it actually touches.:table_structure_ready— this table's structural (DDL) work is done: provided by every structural operation on this table (including:table_ready's and:table_columns_settled's providers).:table_fk_dropped— every foreign key on another table that referenced any column of this table has been dropped (direction: :up). The table-scoped counterpart to:column_fk_dropped, forDropTable, which knows only its own name — not which of its columns other tables' foreign keys point at. Postgres refuses toDROP TABLEwhile another table's foreign key still references it, so the drop must come after thoseDropForeignKeyoperations even thoughDropTableis early-tier (therequired_by_early_tierexemption intoposort_operations/1is what lets that specific dependency win over the blanket barrier).
Column-scoped (key = {schema, table, column}):
:column_ready— a specific column (by its current name) exists.:column_unique_index_removed— a unique index covering this column has been removed.RenameAttributerequires this for the samedown-validity reason as:column_custom_index_removed.:column_custom_index_removed— everyRemoveCustomIndexwhose structuredfieldslist includes this column has run.RenameAttributerequires this for its own column sodownstays valid (see above). Only tracksfields, not rawwhere/expression text that might reference the column without listing it — a known gap, to be closed later by lettingcustom_indexesdeclare the columns a rawwhere/expression touches, rather than by conservatively widening this back to table scope.:column_check_constraint_removed— every check constraint covering this column has been removed.RemoveAttribute/RenameAttributerequire this for their own column sodownstays valid: theirdownrecreates/renames the column back, which must happen beforeRemoveCheckConstraint'sdownrecreates a constraint that needs it — i.e.RemoveCheckConstraintmust run first inup.:column_fk_dropped— every foreign key on another table that referenced this specific column has been dropped (direction: :up). Postgres refuses to drop a unique constraint/primary key that's still referenced by another table's foreign key (verified directly, and not limited to primary keys), soRemovePrimaryKey/RemoveUniqueIndexrequire this for each of their own columns.:reference_index_removed— the reference index on this FK column has been dropped (RemoveReferenceIndex).AddReferenceIndexrequires this for its own source column: both derive the same auto-generated index name from their columns, so rewriting an existing reference index (a Remove/Add pair, e.g. when itsindex_wherepredicate changes) must drop the old index before creating the new one.
Index-scoped (key = {schema, table, index}, same shape as a column-scoped
key but the third element is an index name, not a column):
:custom_index_removed— a specific named custom index has been dropped, so a same-named index can be recreated (Postgres index names are unique per schema).
Column-set-scoped (key = {schema, table, sorted_columns}, the third
element a sorted list of column atoms):
:unique_index_created— a unique index covering exactly this column set exists (AddUniqueIndexfor identities, uniqueAddCustomIndex). The set is the columns the rendered index actually covers — including the tenant attribute that attribute-strategy multitenancy prefixes at render time. A foreign key requires this fact for exactly its referenced column set (destination_attributeplusmatch_withdestinations), matching Postgres's rule that an FK target must be backed by a unique index on exactly the referenced columns — per-column unique indexes don't satisfy it, and correspondingly don't provide this fact.
Summary
Functions
Operation types that must run before every other operation in the batch.
Facts made true once op has run.
Facts that must already be provided (by some other operation) before op can run.