# `AshPostgres.MigrationGenerator.OperationDeps`
[🔗](https://github.com/ash-project/ash_postgres/blob/v2.13.1/lib/migration_generator/operation_deps.ex#L5)

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's `where`, a custom index's expression,
  a check constraint's `check:`) 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`, for `DropTable`, which
  knows only its own name — not which of its columns other tables' foreign
  keys point at. Postgres refuses to `DROP TABLE` while another table's
  foreign key still references it, so the drop must come after those
  `DropForeignKey` operations even though `DropTable` is early-tier (the
  `required_by_early_tier` exemption in `toposort_operations/1` is 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. `RenameAttribute` requires this for the same
  `down`-validity reason as `:column_custom_index_removed`.
- `:column_custom_index_removed` — every `RemoveCustomIndex` whose
  structured `fields` list includes this column has run. `RenameAttribute`
  requires this for its own column so `down` stays valid (see above). Only
  tracks `fields`, not raw `where`/expression text that might reference
  the column without listing it — a known gap, to be closed later by
  letting `custom_indexes` declare the columns a raw `where`/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`/`RenameAttribute`
  require this for their own column so `down` stays valid: their `down`
  recreates/renames the column back, which must happen *before*
  `RemoveCheckConstraint`'s `down` recreates a constraint that needs it —
  i.e. `RemoveCheckConstraint` must run first in `up`.
- `: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), so `RemovePrimaryKey`/`RemoveUniqueIndex`
  require this for each of their own columns.
- `:reference_index_removed` — the reference index on this FK column has
  been dropped (`RemoveReferenceIndex`). `AddReferenceIndex` requires 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 its `index_where` predicate 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 (`AddUniqueIndex` for identities, unique `AddCustomIndex`).
  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_attribute` plus `match_with` destinations),
  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.

# `early_tier?`

Operation types that must run before every other operation in the batch.

# `provides`

Facts made true once `op` has run.

# `requires`

Facts that must already be provided (by some other operation) before `op` can run.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
