feat(efcore): incremental migrations — serialized schema snapshot + differ - #377
Merged
Conversation
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>
This was referenced Jul 18, 2026
…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>
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
EfSchemaSnapshotis 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. Theaddflow 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
ITabletranslation routes throughSnapshotTable.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 theSchemaMigrationswitchboard:AddColumn/AlterColumn(carrying the old definition for correct SQL generation) /DropColumnCreateIndex/DropIndex, recreate on changeDropPrimaryKey+AddPrimaryKeyCreateSequence/DropSequence/AlterSequence(increment changes)EnsureSchema(never dropped on Down — shared with Marten/Wolverine)Sql()blocks from the captured DDLDown()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 ownCreateMigrationAsyncand wraps the update SQL inSql()operations (rollback SQL for Down) — handling everything Weasel can migrate, including partitionAdditive/Rebuilddeltas 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
IMigrationsSqlGeneratoragainst PostgreSQL → Weasel's own delta detection reportsNone→ Down operations roll it back → Weasel again reportsNoneagainst the original model.Full EF suite (PG+SS) green locally: 91 passed.
🤖 Generated with Claude Code