Skip to content

auto_migrate: genuine cross-table FK cycles cannot CREATE on Postgres — defer cycle FKs to post-create ALTER #304

Description

@0x054

Follow-up to #302 / #303, which fixed self-referential FKs (a self-loop is not a cycle and no longer evicts its component from the dependency order).

What still fails

A genuine cross-table cycle — a.b_id → b and b.a_id → a — cannot be dependency-ordered at all. ferro_migrate::order_by_dependencies falls through and appends the remaining tables in input order, and whichever table is emitted first carries an inline REFERENCES to a relation that does not exist yet:

CREATE TABLE "a" (..., "b_id" uuid REFERENCES "b" ("id"));  -- ERROR: relation "b" does not exist
CREATE TABLE "b" (..., "a_id" uuid REFERENCES "a" ("id"));

SQLite tolerates forward FK references at DDL time, so the fallback only bites on Postgres — the same asymmetry that hid #302.

Proposed fix

For tables that remain unordered after the dependency sort, strip the cycle-forming FKs from the CREATE TABLE emission and add them back after all creates via named post-create constraints:

ALTER TABLE "a" ADD CONSTRAINT "fk_a_b_id_b" FOREIGN KEY ("b_id") REFERENCES "b" ("id");

The pieces already exist: FK constraint names are single-sourced in ferro_ddl_lowering::fk_name (AGENTS.md I-1 §6), and the ALTER-path emitter already renders named ADD CONSTRAINT FKs (see emit_sql_with_ir_add_column_fk_postgres). Cross-emitter parity needs a check that the Alembic bridge names these identically (SQLAlchemy handles cycles with use_alter, which produces the same post-create ALTER shape).

Until then the boundary is: self-loops order correctly (#302); genuine cycles are emitted in input order and fail loudly on Postgres at CREATE time.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions