Skip to content

feat(efcore): incremental migrations — serialized schema snapshot + differ - #377

Merged
jeremydmiller merged 6 commits into
masterfrom
feat/ef-incremental-migrations
Jul 18, 2026
Merged

feat(efcore): incremental migrations — serialized schema snapshot + differ#377
jeremydmiller merged 6 commits into
masterfrom
feat/ef-incremental-migrations

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #367. Third phase of the EF Core migration generation epic (#371). Stacked on #376 (emitter), which stacks on #375 (translation layer) — merge in order; retarget as the stack lands.

Snapshot baseline — the Sable lesson, minus the shadow database

EfSchemaSnapshot is a JSON-serialized, design-time snapshot of the Weasel typed model — tables (columns/indexes/FKs/checks/PK), sequences, and raw-SQL objects captured as their CREATE/DROP DDL. It is written beside the generated migrations and never compiled, exactly as the issue specifies. The add flow becomes: deserialize snapshot → diff against the current model entirely in memory → emit the incremental migration → rewrite the snapshot. No Docker, no live DB.

A structural bonus: the snapshot DTOs are now the canonical IR — the #375 ITable translation routes through SnapshotTable.From(...) into shared operation builders, so the first-migration path and the incremental differ share one pipeline and cannot drift.

Delta → operations

EfSnapshotDiffer.Diff(baseline, target) produces incremental Up/Down operations paralleling the SchemaMigration switchboard:

  • Columns → AddColumn / AlterColumn (carrying the old definition for correct SQL generation) / DropColumn
  • Indexes → CreateIndex / DropIndex, recreate on change
  • Foreign keys / check constraints → add/drop pairs, recreate on change
  • Primary key → DropPrimaryKey + AddPrimaryKey
  • Sequences → CreateSequence / DropSequence / AlterSequence (increment changes)
  • New schemas → EnsureSchema (never dropped on Down — shared with Marten/Wolverine)
  • Raw-SQL objects → added/removed via Sql() blocks from the captured DDL
  • Down() is real and runs in reverse order of Up (a new index is dropped before the column it covers)

Changed raw-SQL objects (partitioned tables, function bodies) are refused with guidance rather than guessed at — the snapshot diff cannot infer a safe transform for them. That's what the second mode is for:

Live-database baseline mode

EfSnapshotDiffer.DiffAgainstDatabaseAsync(IDatabase) runs Weasel's own CreateMigrationAsync and wraps the update SQL in Sql() operations (rollback SQL for Down) — handling everything Weasel can migrate, including partition Additive/Rebuild deltas and function changes, per the issue's secondary-mode requirement.

Renames are deliberately not inferred (the Weasel model carries no rename intent); the explicit-rename seam belongs to the CLI phase (#368).

Emitter additions

Renders the incremental operation set: standalone AddColumn/AlterColumn/DropColumn, DropIndex, AddForeignKey/DropForeignKey, Add/DropPrimaryKey, Add/DropCheckConstraint, AlterSequence.

Testing

  • Snapshot acceptance: serialize → deserialize → diff against the unchanged model yields zero operations.
  • Scenario tests: added column (with default), changed index predicate (drop+recreate, rollback restores the original), new table + FK together, altered column with old-definition capture, changed-raw-object refusal, and incremental ops rendering through the emitter with the migration-id monotonicity guard.
  • End-to-end acceptance per the issue: initial generated migration applied via the EF runtime → model changed (add column + index) → snapshot diff → incremental operations executed through the real Npgsql IMigrationsSqlGenerator against PostgreSQL → Weasel's own delta detection reports None → Down operations roll it back → Weasel again reports None against the original model.

Full EF suite (PG+SS) green locally: 91 passed.

🤖 Generated with Claude Code

jeremydmiller and others added 3 commits July 18, 2026 16:10
Closes #365. First implementation phase of the EF Core migration
generation epic (#371), building on the #364 spike results.

- New MigrationOperationTranslation in Weasel.EntityFrameworkCore: walks
  the provider-neutral surface (ITable/ITableColumn/ITableIndex/
  ForeignKeyBase/SequenceBase) and produces EF Core MigrationOperation
  instances — the reverse of MapToTable. Raw store type strings
  (ColumnType) everywhere so EF's CLR mapping is bypassed and DDL matches
  Weasel exactly; the CLR type is a best-effort inverse used only for the
  Column<T>() generic in emitted C#
- CreateTable with nested columns / primary key / check constraints /
  foreign keys, one CreateIndex per index, EnsureSchema per non-default
  schema (deduplicated; default public/dbo emitted as null Schema like
  EF's own scaffolding), CreateSequence from SequenceBase
- Provider specifics: identity → Npgsql:ValueGenerationStrategy or
  SqlServer:Identity annotations; computed columns → ComputedColumnSql +
  IsStored (always stored on PG); index includes/method annotations;
  CascadeAction → ReferentialAction with SQL Server Restrict ≡ NoAction
  mirroring mapDeleteBehavior
- Raw-SQL fallback: non-table/non-sequence objects (functions, sprocs,
  table types) and anything matched by the ForceRawSql hook (e.g.
  partitioned tables) are wrapped in SqlOperation carrying the object's
  own WriteCreateStatement DDL; expression indexes throw with guidance
  to use the hook
- ToDropMigrationOperations for Down() bodies: reverse-order DropTable /
  DropSequence / raw drops; schemas never dropped (may be shared with
  Marten/Wolverine)
- Weasel.Core additions: ITable.Columns and ITableIndex.Columns expose
  the column collections on the neutral surface (implicitly satisfied by
  every provider's concrete types)

13 new DB-free unit tests; all provider suites green locally.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes #366. Second implementation phase of the EF Core migration
generation epic (#371), rendering the #365 operation lists into
compilable attribute-only migration files.

- EfMigrationFileEmitter.EmitMigration renders Up/Down operation lists
  over the stable public MigrationBuilder surface — deliberately not
  EF's pubternal CSharpMigrationsGenerator (dotnet/efcore#23595).
  Generated migrations carry [DbContext]/[Migration] attributes and no
  BuildTargetModel body, per the #364 spike verification
- Renders EnsureSchema, CreateTable (nested columns with raw store
  types, PK incl. composite, check constraints, FKs with referential
  actions), CreateIndex (unique/filter + annotations), CreateSequence,
  Sql (verbatim strings), DropTable, DropSequence; the
  Npgsql:ValueGenerationStrategy annotation is rendered as the real
  NpgsqlValueGenerationStrategy enum literal with the using added on
  demand; unknown operations/annotations throw rather than emitting
  wrong code
- Column names map to anonymous-type members with @-escaping for
  reserved words and name:-argument fallback for non-identifier names
- Migration ids are yyyyMMddHHmmss_Name UTC with a monotonicity guard:
  LastMigrationId bumps the timestamp until the new id sorts strictly
  after (EF orders by plain string sort)
- EmitStubContext generates the no-entity host context: provider
  configured, history table relocated into the critter-stack schema,
  EF 9+ PendingModelChangesWarning suppressed, registration snippet in
  the XML docs, plus an IDesignTimeDbContextFactory reading
  WEASEL_EF_CONNECTION so dotnet ef update/script/bundle work without
  an application host

Testing: the generated sample files (from a Weasel schema with sequence,
identity, checks, FK, filtered index) are CHECKED IN and compiled as part
of the test project — the "generated files compile" acceptance — with a
drift-guard test proving they are byte-for-byte emitter output, and an
end-to-end test applying them through the real EF runtime against
PostgreSQL, round-tripping the schema against Weasel's own delta
detection (no changes), and migrating back down to zero.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes #367. Third implementation phase of the EF Core migration
generation epic (#371).

- EfSchemaSnapshot: JSON-serialized design-time snapshot of the Weasel
  typed model (tables/columns/indexes/FKs/checks/PK, sequences, and
  raw-SQL objects captured as their CREATE/DROP DDL) — Weasel's analog
  of EF's ModelSnapshot, written beside the generated migrations and
  never compiled. The Sable lesson without the shadow database
- The snapshot DTOs are now the canonical IR: the #365 ITable
  translation routes through SnapshotTable.From(...) into shared
  operation builders, so first-migration translation and the
  incremental differ can never drift apart
- EfSnapshotDiffer.Diff(baseline, target): in-memory diff producing
  incremental Up/Down operations — Add/Alter/DropColumn (Alter carries
  the old definition), Create/DropIndex (recreate on change),
  Add/DropForeignKey, Add/DropCheckConstraint, Drop+AddPrimaryKey,
  Create/Drop/AlterSequence, EnsureSchema for new schemas (never
  dropped), and raw-object add/remove via Sql(). Down runs in reverse
  order of Up. Changed raw-SQL objects are refused with guidance —
  the snapshot diff cannot infer a safe transform for partitioned
  tables or function bodies
- EfSnapshotDiffer.DiffAgainstDatabaseAsync: the live-database baseline
  mode — Weasel's own CreateMigrationAsync SQL (updates + rollbacks)
  wrapped in Sql() operations, covering everything the snapshot diff
  refuses (partition additive/rebuild, function changes)
- Emitter renders the incremental operations: AddColumn/AlterColumn/
  DropColumn, DropIndex, AddForeignKey/DropForeignKey standalone,
  Add/DropPrimaryKey, Add/DropCheckConstraint, AlterSequence

Renames are deliberately not inferred (the model carries no rename
intent); the seam arrives with the CLI phase where renames can be
declared explicitly.

Tests: snapshot JSON round-trip yields a zero diff; add-column /
changed-index / new-table+FK / altered-column scenarios; changed raw
object refusal; incremental ops render through the emitter with the id
monotonicity guard; and an end-to-end acceptance test that applies the
initial generated migration via EF, diffs a changed model against the
snapshot, executes the incremental operations through the real Npgsql
migrations SQL generator, has Weasel's own delta detection report None,
then rolls back down and round-trips again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
jeremydmiller and others added 2 commits July 18, 2026 17:36
…ilePath

Deterministic CI builds rewrite [CallerFilePath] to the virtual /_/ source
root, which does not exist on disk — the drift-guard tests failed on CI
with an IO error. Walk up from AppContext.BaseDirectory to the repo root
instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@jeremydmiller
jeremydmiller changed the base branch from feat/ef-migration-emitter to master July 18, 2026 23:10
An error occurred while trying to automatically change base from feat/ef-migration-emitter to master July 18, 2026 23:10
…-migrations

# Conflicts:
#	src/Weasel.EntityFrameworkCore/EfMigrationFileEmitter.cs
#	src/Weasel.EntityFrameworkCore/MigrationOperationTranslation.cs
@jeremydmiller
jeremydmiller merged commit a19ac7b into master Jul 18, 2026
23 checks passed
@jeremydmiller
jeremydmiller deleted the feat/ef-incremental-migrations branch July 18, 2026 23:55
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.

EF migration generation: incremental migrations from TableDelta + serialized schema snapshot

1 participant