Skip to content

fix: order no_phase operations after the objects they act on - #848

Merged
zachdaniel merged 2 commits into
ash-project:mainfrom
MayCXC:fix/no-phase-operation-ordering
Sep 11, 2026
Merged

fix: order no_phase operations after the objects they act on#848
zachdaniel merged 2 commits into
ash-project:mainfrom
MayCXC:fix/no-phase-operation-ordering

Conversation

@MayCXC

@MayCXC MayCXC commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Problem

An operation flagged no_phase: true carries no implicit ordering, so without a requires/1 clause it floats ahead of the phased operations that build a table's structure. Two such operations render SQL against an object another operation creates in the same batch, so floating puts them before that object exists:

  • AlterDeferrability (ALTER CONSTRAINT ... DEFERRABLE) runs against a foreign key an AddAttribute carrying references: creates. When a composite with: key pushes the foreign key into a later phase, the deferrability alter ran first and failed with constraint ... does not exist.
  • AddPrimaryKey (ADD PRIMARY KEY (keys)) runs against key columns an AddAttribute can add in the same batch. Widening a primary key to include a newly added column (list-partitioning by that column, for instance) generated ADD PRIMARY KEY (id, cell_id) before cell_id existed.

Fix

Give each a requires/1 clause: AlterDeferrability{direction: :up} requires table_columns_settled; AddPrimaryKey requires column_ready for each key column. Both requirements are vacuous when the object predates the batch (nothing in the batch provides the fact), and neither adds a cycle: each provides only table_structure_ready, which AddAttribute never requires.

This builds on the dependency-graph model from #798, and is a distinct case from the composite-primary-key work in #805, which addressed the streamline merge and the DropForeignKey{direction: :down} phase split rather than ADD PRIMARY KEY over a newly added column.

The class

The module doc now documents the whole class, so future no_phase operations are held to it. The rest are already safe: they act on objects that predate the batch (RenameTable, MoveTableSchema, RemoveCustomIndex, and the other renames and drops), render nothing in up (the *Down helpers), or are ordered by the consumers that require their facts.

Tests

Reproducing tests for both fixes in operation_deps_test.exs, at both the requires/provides level and the toposort_operations/1 level. Each fails without its requires clause (verified by neutralizing the clause).

  • Full test suite passes.
  • mix ash_postgres.generate_migrations --check is clean for both PG variants, so no checked-in migration reorders.

A migration operation flagged `no_phase: true` carries no implicit ordering
and, without a `requires/1` clause, floats ahead of the phased operations that
build a table's structure. Two such operations render SQL against an object
another operation creates in the same batch, so floating puts them before that
object exists:

  - AlterDeferrability (`ALTER CONSTRAINT ... DEFERRABLE`) runs against a
    foreign key an AddAttribute carrying `references:` creates. A composite
    `with:` key that pushes the foreign key into a later phase left the
    deferrability alter running first, failing with "constraint does not
    exist". Require table_columns_settled.

  - AddPrimaryKey (`ADD PRIMARY KEY (keys)`) runs against key columns an
    AddAttribute can add in the same batch. Widening a primary key to include
    a newly added column (list-partitioning by that column) generated
    `ADD PRIMARY KEY (id, cell_id)` before cell_id existed. Require column_ready
    for each key.

Both requirements are vacuous when the object predates the batch, and neither
adds a cycle: each provides only table_structure_ready, which AddAttribute
never requires.

Document the class in the module doc so future no_phase operations are held to
it, and audit the rest: they act on objects that predate the batch (renames and
drops), render nothing in `up` (the `*Down` helpers), or are ordered by the
consumers that require their facts. Add reproducing tests for both fixes; each
fails without its requires clause.
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` are not grouped into the create/alter

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think a lot of the comments/docs in this PR can be trimmed down significantly. Would you mind making that change? Otherwise LGTM

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure 092c37c

@MayCXC
MayCXC requested a review from zachdaniel September 10, 2026 23:15
@zachdaniel
zachdaniel merged commit 3d00757 into ash-project:main Sep 11, 2026
@zachdaniel

Copy link
Copy Markdown
Contributor

🚀 Thank you for your contribution! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants