Skip to content

dialect/sql/schema: a directory of what the schema cannot state, with a history of its own - #7

Merged
lesomnus merged 1 commit into
masterfrom
migrations-beside
Oct 10, 2026
Merged

lesomnus merged 1 commit into
masterfrom
migrations-beside

Conversation

@lesomnus

Copy link
Copy Markdown
Member

For lesomnus/payday#33: a supported place for DDL the ent schema cannot state.

The problem

An exclusion constraint over a time range, or a foreign key that carries the tenant, is something a database holds and no ent schema can state.

  • In the ent directory, it is what the next Plan proposes to drop. That is by design: planning brings the directory into line with the schema.
  • In a second directory applied with the same Apply, it shares schema_revisions. The two directories then form one history ordered by version. A file written on one branch before a plan made on another is refused as out of order once that plan has run.

What this adds

ent   := entschema.Migrations{Dir: dir, Dialect: dialect.Postgres, Tables: entmigrate.Tables}
extra := ent.Extra(extraDir)          // migrations/extra: written by hand, never planned

ent.Apply(ctx, db, d)                 // the schema first
extra.Apply(ctx, db, d)               // then what stands on it

extra.Write(entschema.Version(time.Now()), "booking_no_overlap", sql) // a new file, and the sum
Migrations.Revisions The table a directory's history is kept in. Empty is schema_revisions, as before. ExtraRevisionTable is schema_revisions_extra.
Migrations.Atop Applies the directory onto a database another directory already built. Without it, atlas refuses the first file because the database holds tables.
Migrations.Extra(dir) Both of the above, with the ent directory's dialect. DefaultExtraDir is migrations/extra. A directory reads its files and not its subdirectories, so nesting it is safe.
Migrations.Write / Rehash / Version Put a hand-written file into a directory and rewrite atlas.sum, so OpenDir opens it without the Atlas CLI. A version already present is refused. Rehash is for a file still being written, never one that has been applied.
Plan Refuses a directory with no Tables (ErrNoSchema). Otherwise it would plan a file that drops every table.

Everything is additive. A Migrations that sets none of the new fields behaves exactly as before.

Tests

TestExtra (SQLite):

  • a hand-written file older than the planned one runs;
  • each history holds only its own versions, and nothing is pending in either;
  • Atop is what lets the first file run on a database the ent directory built;
  • the next ent plan and Check both leave its index alone;
  • Plan refuses the extra directory and writes nothing;
  • Write refuses a version that is already there.

On PostgreSQL 17 I ran the same flow with a throwaway program, since this module has no PostgreSQL driver. A hand-written file older than the plan added a CHECK and a btree_gist EXCLUDE, and applied atop the ent schema. Each history held one row, nothing was pending, Check passed, and re-applying was a no-op.

go test -race passes in dialect and in the integration module under Go 1.27.2, and lint.sh is clean on the three files.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KxtnpRW533bdErnW8QPhsk

… a history of its own

An exclusion constraint over a time range, or a foreign key that carries
the tenant, is something a database holds and no ent schema can state.
Written into the ent directory, it is what the next Plan proposes to
drop. Written into a second directory applied with the same Apply, it
shares schema_revisions, so the two directories are one history ordered
by version: a file written on one branch before a plan made on another is
refused as out of order once that plan has run.

So Migrations can be a directory beside the ent one:

- Revisions names the table its history is kept in, so the two are two
  histories (ExtraRevisionTable for the conventional one);
- Atop applies it onto a database another directory already built, which
  atlas otherwise refuses as not clean;
- Extra(dir) is both, with the ent directory's dialect;
- Write puts a file written by hand into a directory and rehashes its sum,
  so OpenDir opens it without the Atlas CLI, and refuses a version that is
  already there; Rehash is for the file still being written;
- Plan refuses a directory with no Tables (ErrNoSchema), which it would
  otherwise plan into a file that drops every table.

Checked: TestExtra on SQLite -- a file older than the plan runs, the
histories hold only their own, Atop is what lets it start, the next plan
and Check leave it alone, there is nothing to plan; and the same on
PostgreSQL 17 with a CHECK and an EXCLUDE (a throwaway program, since
this module has no PostgreSQL driver). go test -race in dialect and in
the integration module under Go 1.27.2; lint.sh on the three files.

Reported as lesomnus/payday#33.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KxtnpRW533bdErnW8QPhsk
@lesomnus
lesomnus merged commit 979efce into master Oct 10, 2026
8 checks passed
@lesomnus
lesomnus deleted the migrations-beside branch October 10, 2026 08:35
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.

1 participant