feat: computed-column introspection + delta detection (PostgreSQL + SQL Server) - #373
Merged
Merged
Conversation
…QL Server) Completes #363. The previous round added computed-column emission and the EF Core HasComputedColumnSql mapping; this round closes the loop so computed columns fully round-trip through delta detection: - FetchExisting reads generated-column metadata: is_generated / generation_expression from information_schema on PostgreSQL, sys.computed_columns (definition + is_persisted) on SQL Server - Delta detection compares computed columns by canonicalized expression (reusing TableCheckConstraint.Canonicalize) — SQL Server also compares the PERSISTED flag, and skips the declared type since the server derives it from the expression; PostgreSQL keeps the declared-type comparison - Conservative in both directions: only model-declared computed columns participate, and actual computed columns unknown to the model are left untouched (mirrors the unknown-check-constraint handling) - A changed definition migrates by drop + re-add (definitions can't be altered in place; the data is derived so the rewrite is lossless), with matching rollback support - CanAdd allows NOT NULL computed columns (the database back-fills them) - New fluent builders: PostgreSQL GeneratedAs(expr), SQL Server ComputedAs(expr, persisted) mirroring SQLite's GeneratedAs - 6 new integration tests per provider; docs sections + samples; refreshed the stale EFCORE_IMPROVEMENTS.md known-gaps list Closes #363 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Jul 18, 2026
jeremydmiller
added a commit
that referenced
this pull request
Jul 19, 2026
* feat(efcore): Weasel model → EF MigrationOperation translation layer 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> * feat(efcore): C# migration file emitter + stub DbContext 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> * feat(efcore): incremental migrations — serialized snapshot + differ 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> * feat(efcore): db-ef-migration add | script | baseline CLI command Closes #368. Fourth implementation phase of the EF Core migration generation epic (#371). - New db-ef-migration JasperFx command in Weasel.EntityFrameworkCore (discovered via [assembly: JasperFxAssembly]; Weasel.Core stays EF-free), using the same WeaselInput / TryChooseSingleDatabase database-selection machinery as db-patch — IDatabase is the single source of schema objects, so Marten/Wolverine/Polecat tables all flow in through one door - `add <Name>`: first run scaffolds the stub context (history table relocated into the first non-default schema of the database's objects), the initial create-everything migration, and the JSON snapshot; later runs diff against the snapshot (or the live database with --against-database) and emit an incremental migration with the id monotonicity guard. --output/--namespace/--context/ --history-schema flags - `script`: documents the canonical EF toolchain path (dotnet ef migrations script --idempotent / bundle) verified by the #364 spike — idempotent scripting needs the compiled migrations, which only exist in the consuming project - `baseline`: adopts a pre-existing database by inserting __EFMigrationsHistory rows (create-if-missing relocated history table) for every generated migration file without executing them — the EF-sanctioned baselining technique, idempotent across runs - EfMigrationGenerator is the testable engine behind the command: provider detection from the Migrator type, structural partition detection as the default ForceRawSql routing (no provider references), connection resolution via IConnectionSource Tests: provider detection, partition detection, first-run scaffold → no-change no-op → model change → incremental add with ordered ids, and baselining against live PostgreSQL (rows recorded once, idempotent second pass, verified in the relocated history table). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(efcore): inverted schema-comparison validation harness Closes #369. Fifth implementation phase of the EF Core migration generation epic (#371). - InvertedComparisonHarness: the reverse of SchemaComparisonHarness — schemas defined as Weasel objects, the generated migration chain (initial + snapshot-diffed incrementals) COMPILED WITH ROSLYN and applied through the real EF runtime via Migrate(), then validated by (a) catalog-level SchemaComparer parity against a Weasel-created schema using the existing neutral introspectors and (b) Weasel's own SchemaMigration.DetermineAsync reporting None against the EF-migrated database. PostgreSQL and SQL Server variants - Scenarios: baseline conventions (identity, defaults, varchar facets, unique+filtered index, FK cascade, check constraint), computed columns, raw-SQL fallback objects (list-partitioned table + plpgsql function via Sql() blocks + sequence), a two-migration incremental chain (add column + index + new table), coexistence of two generated migration sets with separate schemas/history tables in one database, and a SQL Server baseline - Two generator fixes surfaced by the harness: - generated migration files now emit `using System;` (they must be self-contained rather than relying on ImplicitUsings) - SQL Server unique indexes without an explicit predicate are emitted as raw CREATE UNIQUE INDEX DDL — EF's SqlServer generator auto-appends a WHERE col IS NOT NULL filter whenever the (empty) target model cannot prove the columns non-nullable, which would diverge from Weasel's index - CI: the new suites live in Weasel.EntityFrameworkCore.Tests, which ci-build-efcore.yml already runs against PostgreSQL + SQL Server on net9.0/net10.0 — no workflow change needed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * docs(efcore): EF Core migration generation documentation Closes #370. Final phase of the EF Core migration generation epic (#371). - New docs/efcore/migration-generation.md: overview + when to use which flow (Weasel-native vs EF-artifact generation), the three generated artifacts, getting-started walkthrough (db-ef-migration add -> dotnet ef database update), incremental migrations + snapshot, live-database baseline mode, adopting existing databases via baseline, translation-layer API, and the limitations / raw-SQL fallback boundaries (partitioning, functions, expression indexes, SS unique-index filter behavior, no ef migrations add/remove against the stub, SQLite exclusion, PendingModelChangesWarning explanation) - New docs/efcore/migration-coexistence.md: mixed EF + Marten/ Wolverine/Polecat apps — two migration streams, relocated history table, --context usage, the single-owner rule with ExcludeFromMigrations, EF projection round-trip ownership guidance, and how the harnesses verify coexistence - docs/efcore/migrations.md now positions the two directions side by side; VitePress nav updated - All code samples are mdsnippets sourced from the new compilable DocSamples/EfCoreMigrationSamples.cs per repo convention - CLAUDE.md project structure + EFCORE_IMPROVEMENTS.md capability record updated Computed-column docs for the PG/SS table-modeling pages shipped with the computed-column PR (#373). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(tests): resolve SampleGenerated path from output dir, not CallerFilePath 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> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
jeremydmiller
added a commit
that referenced
this pull request
Jul 30, 2026
…lta (#400) Regression from #373. Routing every column through MatchesForDelta replaced a null comparer — under which ItemDelta fell back to the VIRTUAL Equals(object) — with a call to Equals(actual) that binds at compile time to the protected, non-virtual Equals(TableColumn) overload. Any subclass override of Equals(object) was silently bypassed. That override is a real extension seam: Marten's RevisionColumn uses it to declare that an integer mt_version tolerates a column an earlier release already migrated to bigint, instead of emitting a lossy narrowing cast (marten#4614 / #4742). With the override skipped, the column landed in Columns.Different, so the table was classified as needing an Update while the writer still emitted nothing for it — AssertDatabaseMatchesConfiguration threw with an empty change set. Bisected to 9.18.0; 9.17.0 is clean. Both the PostgreSQL and SQL Server implementations now compare through the virtual member. No behaviour change for columns that do not override Equals(object), since the base override delegates to Equals(TableColumn). Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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 #363 (part of the EF Core migration generation epic #371).
PR #372 added computed-column emission (
ITableColumn.ComputedExpression/ComputedColumnIsStored) and the EF CoreHasComputedColumnSqlmapping. This PR closes the loop on #363's remaining scope — introspection and delta detection — so computed columns fully round-trip: create via Weasel →FindDeltaAsyncreportsNone.What's here
FetchExistingnow reads generated-column metadata —is_generated/generation_expressionfrominformation_schema.columnson PostgreSQL, a newsys.computed_columnsresult set (definition +is_persisted) on SQL Server.TableCheckConstraint.Canonicalize, which already handles the catalogs' cast/paren/bracket rewriting). SQL Server also compares the PERSISTED flag and skips the declared type (the server derives it from the expression; the type is never emitted). PostgreSQL keeps the declared-type comparison since generated columns declare one.CanAddnow permits NOT NULL computed columns since the database back-fills them.GeneratedAs(expression)on PostgreSQL andComputedAs(expression, persisted)on SQL Server, mirroring SQLite's existingGeneratedAs.EFCORE_IMPROVEMENTS.md.PostgreSQL VIRTUAL note
The model carries
ComputedColumnIsStored, but PostgreSQL emission always writesSTORED(documented on the property): VIRTUAL only arrives in PG 18 and Weasel has no server-capability context at DDL-generation time. The flag is deliberately excluded from PG delta comparison so it can't cause perpetual diffs. Revisit emission gating when PG 18 support lands.Testing
computed_columns.csin both test projects): fetch-existing reads the definition, no-op round trip, changed expression →Update→ patched toNone, changed PERSISTED flag (SS), adding a computed column to an existing table, and undeclared computed columns being left alone.🤖 Generated with Claude Code