#270: Unify Table/TableColumn model + pluggable DDL syntax strategy (Weasel 9.0)#271
Merged
Merged
Conversation
…alpha.3 Pulls in the latest pre-release alphas of JasperFx 2.0 ahead of #270 implementation work. Verified clean build across the solution and all non-Docker test suites pass: - Weasel.Core.Tests: 6/6 - Weasel.Sqlite.Tests: 361/361 - Weasel.CommandLine.Tests: 23/23 (with Postgres container) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… providers
Adds two new base classes in Weasel.Core that consolidate the boilerplate every
schema object reimplements today:
- SchemaObjectBase: implements ISchemaObject with default AllNames() (yield
Identifier), default "single COUNT(*) row" CreateDeltaAsync, and the standard
FindDeltaAsync(DbConnection) pattern that builds a query command, executes it,
resolves the delta, and closes the reader. ReadExistsCountAsync is a virtual
hook that converts the count to long via Convert.ToInt64 — handles the int /
long / decimal variation across SQL Server / MySQL / Oracle so no provider
needs to override the delta logic anymore.
- SequenceBase: extends SchemaObjectBase with the cross-provider sequence
properties: nullable StartWith, plus Owner / OwnerColumn for providers that
support OWNED BY.
Refactors the 4 provider Sequence subclasses to inherit from SequenceBase:
- Postgresql.Sequence: 84 → 55 LOC
- SqlServer.Sequence: 86 → 55 LOC
- Oracle.Sequence: 104 → 76 LOC (Oracle keeps its provider-specific
OracleConnection FindDeltaAsync overload
and its PL/SQL DROP wrapper)
- MySql.Sequence: 65 → 58 LOC (gains the standard IInheritance, keeps
its table-based emulation override)
Each provider's concrete subclass now only contains the 3 things that genuinely
vary per database — WriteCreateStatement, WriteDropStatement,
ConfigureQueryCommand — plus a strongly-typed FindDeltaAsync overload (e.g.
NpgsqlConnection) where the original surface had one.
No behavioural change. All 15 Postgresql Sequence tests pass; all 6 SqlServer
Sequence tests pass.
Sets the foundation for FunctionBase, ViewBase, and other ISchemaObject
implementations in subsequent commits.
Refs #270.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Lifts the ISchemaObject boilerplate that both Function implementations were duplicating: - Constructor pattern (DbObjectName, body, optional drop statements) - IsRemoved flag with protected setter - Body(Migrator?) — routes through the provider's WriteCreateStatement - DropStatements() — returns pre-supplied drops, empty for IsRemoved, or ComputeDefaultDropStatements() (abstract per provider) - WriteDropStatement — iterates DropStatements() and writes each line - CreateDeltaAsync — reads existing function via ReadExistingFromReaderAsync (abstract per provider) and routes through CreateFunctionDelta (abstract) - AllNames, Identifier — inherited from SchemaObjectBase Each provider's concrete Function now only contains: - WriteCreateStatement (PG writes RawBody directly; SS wraps in EXEC sp_executesql) - ConfigureQueryCommand (pg_proc + pg_namespace vs. sys.sql_modules) - ReadExistingFromReaderAsync (PG reads 2 result sets; SS reads 1) - ComputeDefaultDropStatements (PG parses function signature; SS uses identifier) - GetDefaultMigrator (provider's migrator type) - ParseIdentifier / ParseSignature (PG has the signature parser; SS uses identifier-only) - FetchExistingAsync / FindDeltaAsync (strongly-typed connection overloads) - ForSql / ForRemoval static factories - PG-only: BuildTemplate / WriteTemplate (template-based function generation) Result: - Postgresql.Functions.Function: 230 → 185 LOC (-45) - SqlServer.Functions.Function: 155 → 109 LOC (-46) - Weasel.Core.FunctionBase: 0 → 133 LOC (new shared base) Net: 91 LOC removed from per-provider Function files, 133 LOC of new reusable infrastructure. All 32 Postgresql Function tests pass; all 20 SqlServer Function tests pass. Refs #270. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…Delta Add Weasel.Core/ViewBase (extends SchemaObjectBase) owning the SELECT body (ViewSql), the MoveToSchema/WithSchema template-method pair, and the ToBasicCreateViewSql helper. Both PostgreSQL.View and SQLite.View now inherit from it and only carry the provider-specific DDL. PostgreSQL View keeps its protected virtual ViewType/ViewKind/GetCreationOptions hooks so MaterializedView continues to override "VIEW"→"MATERIALIZED VIEW", 'v'→'m', and USING <access_method>. SQLite ViewDelta now inherits from SchemaObjectDelta<View> instead of hand-rolling ISchemaObjectDelta. To preserve its "no previous state to restore" no-op semantics when Actual is null, SchemaObjectDelta<T>. WriteRestorationOfPreviousState is now virtual (it was concrete and would NRE on a null Actual). No existing override conflicts — grep across src shows the new SQLite ViewDelta override is the only one. Tests pass with no behaviour change: - SQLite view tests: 38/38 (net10.0) - PostgreSQL view tests: 11/11 (net10.0) - PostgreSQL sequence + function + materialized-view tests: 47/47 - SQL Server sequence + function tests: 26/26 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The five provider ForeignKey classes (PG, SQL Server, Oracle, MySQL, SQLite) were carrying ~120 LOC of near-identical boilerplate each. This commit consolidates the cross-provider parts into ForeignKeyBase: - Parse(string definition[, string? defaultSchema]) — fully shared; subclasses only supply ParseLinkedTable(string) to wrap the parsed table name in the provider-specific DbObjectName subclass. ParseCascadeClause helper reads ON DELETE / ON UPDATE clauses (PG keeps a `new` overload defaulting schema to "public" for catalog rows that hand back unqualified names). - LinkColumns(columnName, referencedName) — append a (dependent, principal) pair to ColumnNames/LinkedNames. MySQL's setter-driven List<string> still works correctly through the base because the setters re-marshal. - ReadReferentialActions(onDelete, onUpdate) — translates catalog cascade text via a virtual ReadAction default that handles every spelling used across the five providers (CASCADE / RESTRICT / NO_ACTION / NO ACTION / SET_NULL / SET NULL / SET_DEFAULT / SET DEFAULT). The static *Provider.ReadAction methods stay in place for external callers. - Equals / GetHashCode — structural across same-provider FKs (name + column lists + LinkedTable + both cascades). Parametrised on virtual NameComparer/ColumnComparer so case-folding providers (Oracle, SQLite, MySQL) just override the comparers. Hash drops the buggy array-reference hashing the per-provider versions had — array identity hashing gave different hashes to structurally-equal FKs, breaking HashSet/Dictionary; omitting the arrays gives more collisions but keeps semantics correct. Equality is widened from the previous strict same-type to "same provider root" — Equals(object?) walks up the inheritance chain to find the immediate subclass of ForeignKeyBase (the provider's concrete ForeignKey class) and uses IsAssignableFrom, so consumer-supplied marker subclasses (Marten ships one as `ForeignKeyTest` in PG tests) still compare equal to plain provider FKs as they did pre-9.0, while cross-provider comparison still returns false. This restores a regression a strict GetType()==GetType() check would have introduced. MisconfiguredForeignKeyException was defined identically and unused in four providers; the canonical type now lives in Weasel.Core and the provider duplicates are removed. (The PG-specific InvalidForeignKeyException and TryToCorrectForLink stay where they are.) MySQL's IsEquivalentTo is kept as a forwarder to the new EqualsCore for backward compatibility. Not consolidated (deferred): WriteAddStatement / WriteDropStatement / ToDDL all take a provider-specific Table parameter, blocked on TableBase (step 9). The obsolete OnDelete/OnUpdate shim properties stay in the subclasses since their per-provider local CascadeAction enums are already marked [Obsolete] and scheduled for removal. Net diff: -189 LOC across the FK files. All tests pass: - PostgreSQL: 29/29 FK + 181/182 detecting_table_deltas (1 unrelated skip) - SQL Server: 22/22 FK + 31/31 detecting_table_deltas - SQLite: 24/24 FK + 361/361 full suite - Weasel.Core: 6/6 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
MySQL.TableDelta was the only provider TableDelta still implementing ISchemaObjectDelta by hand (PG, SQL Server, Oracle, SQLite all inherit from SchemaObjectDelta<Table>). This commit brings MySQL onto the same pattern so the five deltas now compose uniformly: - Constructor forwards (expected, actual) to base - Differencing happens in the protected compare(expected, actual) override (the base ctor calls it and stores the SchemaPatchDifference on Difference); side-effect-populates Columns/Indexes/ForeignKeys/ PrimaryKeyDifference as before - WriteUpdate / WriteRollback overrides preserve the MySQL-specific DDL (MODIFY COLUMN, backtick quoting, DROP FOREIGN KEY syntax, PartitionStrategy = SchemaPatchDifference.Invalid path) - WriteRestorationOfPreviousState overridden because MySQL has historically no-op'd when Actual is null (the base default would NRE) - Expected/Actual properties drop from this file — inherited from SchemaObjectDelta<T> The public surface stays compatible: Columns / Indexes / ForeignKeys / PrimaryKeyDifference / HasChanges are still public (HasChanges is no longer required by ISchemaObjectDelta but is kept for callers that ask "anything to do?" without unpacking the Difference enum). All MySQL tests pass: 188/188 (33 integration tests against MySQL 8.0 container, 155 unit tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The five provider ResolveDatabaseType + ResolveXxxDbType methods were all carrying the same memo-lookup prologue: try DatabaseTypeMemo / ParameterTypeMemo; if absent and the type is Nullable<T>, look up T instead, copy the result to the nullable's slot for next-time, and return. Only the fallback after that miss differs per provider (PG → Npgsql plugin type map; SS/Oracle/MySQL → NotSupportedException; SQLite → null/Text). Move the lookup-with-nullable-promote into DatabaseProvider as two shared protected helpers — ResolveDatabaseTypeFromMemo and ResolveParameterTypeFromMemo — and rewrite each provider's two Resolve* methods as one-or-two-liners that combine the helper with the provider- specific fallback. The audit also suggested templating determineParameterType but I held back: in SS/Oracle/MySQL/SQLite the post-Resolve branches (IsEnum, IsArray, DBNull, IsConstructedGenericType) are largely unreachable because their Resolve always returns a sentinel fallback rather than null. Removing that dead code would technically preserve behaviour but risks edge cases that aren't covered by the current tests, so I left those bodies alone. Net diff: -97 LOC across the five provider files. No behaviour change. Tests pass: - PostgreSQL: 69/69 (Provider + TypeMapping + ToParameterType filter) - SQL Server: 8/8 (+8 pre-existing skips) - SQLite: 45/45 - MySQL: 37/37 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces IDdlSyntaxStrategy in Weasel.Core — the pluggable strategy object the #270 plan calls out for owning provider-specific DDL syntax decisions. Step 8 deliberately covers only the parts of CREATE / DROP that genuinely differ between PostgreSQL and SQLite (the two providers at opposite ends of the feature spectrum, per the audit's "prototype against the extremes" guidance). If the shape holds for those two, step 9 will extend it across the remaining three providers when the full CREATE algorithm moves into TableBase. Strategy surface (intentionally minimal for the prototype): QuoteIdentifier(name) — provider quoting rules WriteDropTable(writer, identifier) — PG appends CASCADE, SQLite doesn't WriteCreateTableHeader(writer, id, style) — CREATE TABLE [IF NOT EXISTS] x ( InlineForeignKeyConstraints — true for SQLite (no ALTER ADD FK) AutoIncrementToken — SERIAL / AUTOINCREMENT / ... StatementTerminator — ";", or "/" for Oracle PostgresqlDdlSyntax (singleton) implements the PG side; SqliteDdlSyntax the SQLite side. Both Table classes expose Syntax => instance and route WriteDropStatement plus the CREATE-header part of WriteCreateStatement through it. The remaining CREATE body (columns / PK / inline FKs / indexes) is unchanged for now — it'll move into TableBase in step 9 once we've validated the strategy shape is workable. This is a behaviour-preserving change: the emitted SQL is byte-for-byte identical to before. Tests pass: - PostgreSQL: 185/188 (3 pre-existing concurrent-index skips) - SQLite: 361/361 (full suite) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces Weasel.Core.TableBase<TColumn, TIndex, TForeignKey> — the
cross-provider base for the five concrete Table classes that the audit
predicted would absorb ~600-700 LOC of duplication. This commit lifts
the high-confidence shared boilerplate; the full CREATE algorithm body
is still per-provider for now (step 8's IDdlSyntaxStrategy will absorb
that incrementally once the strategy shape has settled).
TableBase owns:
- _columns field + Columns getter (generic over TColumn : ITableColumn)
- ForeignKeys + Indexes + IgnoredIndexes
- MaxIdentifierLength + TruncatedNameIdentifier
- ColumnFor / HasColumn / IndexFor / HasIndex / HasIgnoredIndex / IgnoreIndex
- RemoveColumn (always case-insensitive — matches every provider's
pre-existing impl, distinct from the per-provider NameComparison)
- PrimaryKeyName with DefaultPrimaryKeyName() hook (PG/SS use
pkey_{name}_{cols}, Oracle/MySQL use pk_{name}_{cols}, SQLite uses
pk_{name})
- NameComparison virtual (Ordinal default — PG only; SS/Oracle/MySQL/
SQLite override to OrdinalIgnoreCase to match their pre-existing
ColumnFor/HasColumn behaviour)
- ToString() returning $"Table: {Identifier}" (PG/SQLite had this
override; SS/Oracle/MySQL inherit it now)
- ToBasicCreateTableSql() template via GetDefaultMigratorForBasicSql() hook
- All ITable explicit interface implementations (AddColumn(string,string),
AddColumn(string,Type), AddPrimaryKeyColumn variants, ForeignKeys,
AddForeignKey) — providers supply just three abstract hooks:
CreateForeignKey(name), AddColumnAndReturn(name,type),
AddPrimaryKeyColumnAndReturn(name,type), and GetDatabaseTypeFor(Type).
PrimaryKeyColumns stays abstract per provider — PG / SQLite store an
explicit _primaryKeyColumns field, SS / Oracle / MySQL derive from
Columns.Where(IsPrimaryKey). Both shapes preserved.
Provider-specific items retained:
- Constructor (provider wraps Identifier in its DbObjectName subclass)
- WriteCreateStatement / WriteDropStatement (step 8 strategy partially
consumes; rest stays per-provider until follow-up)
- AllNames() override (provider-specific DbObjectName wrapping for
indexes and FKs)
- ColumnExpression nested class
- AddColumn fluent overloads returning ColumnExpression
- PrimaryKeyDeclaration() (provider-specific quoting / formatting)
- Partition properties / Engine / Charset / WithoutRowId / StrictTypes
/ etc.
SQLite's TableDelta touched _columns directly via internal access;
swapped those to call the public AddColumn(TableColumn) factory which
properly sets Parent and adds via the base list.
IgnoredIndexes was previously a PG- and SQLite-only feature; now uniform
across all providers via TableBase. Empty set on SS / Oracle / MySQL is
a no-op until those providers wire it up.
Diff: -398 LOC net across the five Table.cs files (-565 deletions,
+167 insertions excluding the new TableBase.cs).
All tests pass:
- PostgreSQL: 653/656 (3 pre-existing skips)
- SQL Server: 244/252 (8 pre-existing skips)
- SQLite: 361/361
- MySQL: 188/188
- Weasel.Core: 6/6
- Oracle: build-only (no container available locally)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…atrix
Standardises the identity / auto-increment fluent spelling. Before 9.0 the
same conceptual operation was spelled four different ways across providers:
PostgreSQL: .Serial() / .BigSerial() / .SmallSerial()
SQL Server: .AutoNumber()
Oracle: .AutoNumber()
MySQL: .AutoNumber()
SQLite: .AutoIncrement()
In 9.0 every provider's ColumnExpression exposes a canonical .AutoIncrement()
(plus .BigAutoIncrement() / .SmallAutoIncrement() on PostgreSQL for the 64-bit
and 16-bit variants). The historical names are kept as [Obsolete] aliases for
one major-version cycle, with deprecation messages pointing at the canonical
name.
Polymorphic schema-building code now works without provider-specific
casts:
table.AddColumn<int>("id").AsPrimaryKey().AutoIncrement(); // works on all 5
SQL emitted is unchanged per provider — only the C# spelling on the
caller side is normalised.
Also adds docs/core/provider-trait-matrix.md (linked in the sidebar) that
explicitly documents the per-provider differences the audit catalogued:
quoting, case-folding, ALTER TABLE limitations, partitioning, JSON storage,
identity spelling, primary-key name defaults, the new TableBase /
ForeignKeyBase / SequenceBase / FunctionBase / ViewBase / IDdlSyntaxStrategy
class hierarchy, and the constructor pattern. End users can now answer
"what works on every provider vs what's provider-specific" without reading
source.
DocSamples updated to use the canonical AutoIncrement() spelling. Test
code still uses the old names (Serial() / AutoNumber()) for now to validate
the [Obsolete] forwarders work — those warnings will surface in CI as a
gentle nudge to migrate but they're not failures.
This completes the #270 work plan. Sequence of commits on this branch:
83cf822 JasperFx 2.0.0-alpha.8 upgrade
5ea60f3 step 1: SchemaObjectBase + SequenceBase
639628f step 2: FunctionBase
2967556 step 3: ViewBase + SQLite ViewDelta
e7afae8 step 4: ForeignKeyBase (-174 LOC net)
02a6440 step 5: MySQL TableDelta ( +5 LOC net)
9c49c1e step 7: DatabaseProvider memo helpers ( -37 LOC net)
1319cf4 step 8: IDdlSyntaxStrategy prototype
99485c1 step 9: TableBase (-154 LOC net)
<this> step 10: AutoIncrement + trait matrix
Step 6 (TableDeltaBase shared helpers) was absorbed into step 9 since
the helpers needed TableBase to exist before they could decompose
cleanly.
All tests pass:
- PostgreSQL: 653/656 (3 pre-existing skips)
- SQL Server: 244/252 (8 pre-existing skips)
- SQLite: 361/361
- MySQL: 188/188
- Weasel.Core: 6/6
- Oracle: build-clean (no container locally)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 12, 2026
jeremydmiller
added a commit
that referenced
this pull request
May 13, 2026
* #270: Unify Table/TableColumn model + pluggable DDL syntax strategy (Weasel 9.0) (#271) * Bump JasperFx 2.0.0-alpha.1 → 2.0.0-alpha.8, JasperFx.Events → 2.0.0-alpha.3 Pulls in the latest pre-release alphas of JasperFx 2.0 ahead of #270 implementation work. Verified clean build across the solution and all non-Docker test suites pass: - Weasel.Core.Tests: 6/6 - Weasel.Sqlite.Tests: 361/361 - Weasel.CommandLine.Tests: 23/23 (with Postgres container) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 1: SchemaObjectBase + SequenceBase, refactor all 4 Sequence providers Adds two new base classes in Weasel.Core that consolidate the boilerplate every schema object reimplements today: - SchemaObjectBase: implements ISchemaObject with default AllNames() (yield Identifier), default "single COUNT(*) row" CreateDeltaAsync, and the standard FindDeltaAsync(DbConnection) pattern that builds a query command, executes it, resolves the delta, and closes the reader. ReadExistsCountAsync is a virtual hook that converts the count to long via Convert.ToInt64 — handles the int / long / decimal variation across SQL Server / MySQL / Oracle so no provider needs to override the delta logic anymore. - SequenceBase: extends SchemaObjectBase with the cross-provider sequence properties: nullable StartWith, plus Owner / OwnerColumn for providers that support OWNED BY. Refactors the 4 provider Sequence subclasses to inherit from SequenceBase: - Postgresql.Sequence: 84 → 55 LOC - SqlServer.Sequence: 86 → 55 LOC - Oracle.Sequence: 104 → 76 LOC (Oracle keeps its provider-specific OracleConnection FindDeltaAsync overload and its PL/SQL DROP wrapper) - MySql.Sequence: 65 → 58 LOC (gains the standard IInheritance, keeps its table-based emulation override) Each provider's concrete subclass now only contains the 3 things that genuinely vary per database — WriteCreateStatement, WriteDropStatement, ConfigureQueryCommand — plus a strongly-typed FindDeltaAsync overload (e.g. NpgsqlConnection) where the original surface had one. No behavioural change. All 15 Postgresql Sequence tests pass; all 6 SqlServer Sequence tests pass. Sets the foundation for FunctionBase, ViewBase, and other ISchemaObject implementations in subsequent commits. Refs #270. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 2: FunctionBase, refactor PostgreSQL + SQL Server Function Lifts the ISchemaObject boilerplate that both Function implementations were duplicating: - Constructor pattern (DbObjectName, body, optional drop statements) - IsRemoved flag with protected setter - Body(Migrator?) — routes through the provider's WriteCreateStatement - DropStatements() — returns pre-supplied drops, empty for IsRemoved, or ComputeDefaultDropStatements() (abstract per provider) - WriteDropStatement — iterates DropStatements() and writes each line - CreateDeltaAsync — reads existing function via ReadExistingFromReaderAsync (abstract per provider) and routes through CreateFunctionDelta (abstract) - AllNames, Identifier — inherited from SchemaObjectBase Each provider's concrete Function now only contains: - WriteCreateStatement (PG writes RawBody directly; SS wraps in EXEC sp_executesql) - ConfigureQueryCommand (pg_proc + pg_namespace vs. sys.sql_modules) - ReadExistingFromReaderAsync (PG reads 2 result sets; SS reads 1) - ComputeDefaultDropStatements (PG parses function signature; SS uses identifier) - GetDefaultMigrator (provider's migrator type) - ParseIdentifier / ParseSignature (PG has the signature parser; SS uses identifier-only) - FetchExistingAsync / FindDeltaAsync (strongly-typed connection overloads) - ForSql / ForRemoval static factories - PG-only: BuildTemplate / WriteTemplate (template-based function generation) Result: - Postgresql.Functions.Function: 230 → 185 LOC (-45) - SqlServer.Functions.Function: 155 → 109 LOC (-46) - Weasel.Core.FunctionBase: 0 → 133 LOC (new shared base) Net: 91 LOC removed from per-provider Function files, 133 LOC of new reusable infrastructure. All 32 Postgresql Function tests pass; all 20 SqlServer Function tests pass. Refs #270. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 3: ViewBase, refactor PostgreSQL + SQLite View, SQLite ViewDelta Add Weasel.Core/ViewBase (extends SchemaObjectBase) owning the SELECT body (ViewSql), the MoveToSchema/WithSchema template-method pair, and the ToBasicCreateViewSql helper. Both PostgreSQL.View and SQLite.View now inherit from it and only carry the provider-specific DDL. PostgreSQL View keeps its protected virtual ViewType/ViewKind/GetCreationOptions hooks so MaterializedView continues to override "VIEW"→"MATERIALIZED VIEW", 'v'→'m', and USING <access_method>. SQLite ViewDelta now inherits from SchemaObjectDelta<View> instead of hand-rolling ISchemaObjectDelta. To preserve its "no previous state to restore" no-op semantics when Actual is null, SchemaObjectDelta<T>. WriteRestorationOfPreviousState is now virtual (it was concrete and would NRE on a null Actual). No existing override conflicts — grep across src shows the new SQLite ViewDelta override is the only one. Tests pass with no behaviour change: - SQLite view tests: 38/38 (net10.0) - PostgreSQL view tests: 11/11 (net10.0) - PostgreSQL sequence + function + materialized-view tests: 47/47 - SQL Server sequence + function tests: 26/26 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 4: pull ForeignKey shared methods into ForeignKeyBase The five provider ForeignKey classes (PG, SQL Server, Oracle, MySQL, SQLite) were carrying ~120 LOC of near-identical boilerplate each. This commit consolidates the cross-provider parts into ForeignKeyBase: - Parse(string definition[, string? defaultSchema]) — fully shared; subclasses only supply ParseLinkedTable(string) to wrap the parsed table name in the provider-specific DbObjectName subclass. ParseCascadeClause helper reads ON DELETE / ON UPDATE clauses (PG keeps a `new` overload defaulting schema to "public" for catalog rows that hand back unqualified names). - LinkColumns(columnName, referencedName) — append a (dependent, principal) pair to ColumnNames/LinkedNames. MySQL's setter-driven List<string> still works correctly through the base because the setters re-marshal. - ReadReferentialActions(onDelete, onUpdate) — translates catalog cascade text via a virtual ReadAction default that handles every spelling used across the five providers (CASCADE / RESTRICT / NO_ACTION / NO ACTION / SET_NULL / SET NULL / SET_DEFAULT / SET DEFAULT). The static *Provider.ReadAction methods stay in place for external callers. - Equals / GetHashCode — structural across same-provider FKs (name + column lists + LinkedTable + both cascades). Parametrised on virtual NameComparer/ColumnComparer so case-folding providers (Oracle, SQLite, MySQL) just override the comparers. Hash drops the buggy array-reference hashing the per-provider versions had — array identity hashing gave different hashes to structurally-equal FKs, breaking HashSet/Dictionary; omitting the arrays gives more collisions but keeps semantics correct. Equality is widened from the previous strict same-type to "same provider root" — Equals(object?) walks up the inheritance chain to find the immediate subclass of ForeignKeyBase (the provider's concrete ForeignKey class) and uses IsAssignableFrom, so consumer-supplied marker subclasses (Marten ships one as `ForeignKeyTest` in PG tests) still compare equal to plain provider FKs as they did pre-9.0, while cross-provider comparison still returns false. This restores a regression a strict GetType()==GetType() check would have introduced. MisconfiguredForeignKeyException was defined identically and unused in four providers; the canonical type now lives in Weasel.Core and the provider duplicates are removed. (The PG-specific InvalidForeignKeyException and TryToCorrectForLink stay where they are.) MySQL's IsEquivalentTo is kept as a forwarder to the new EqualsCore for backward compatibility. Not consolidated (deferred): WriteAddStatement / WriteDropStatement / ToDDL all take a provider-specific Table parameter, blocked on TableBase (step 9). The obsolete OnDelete/OnUpdate shim properties stay in the subclasses since their per-provider local CascadeAction enums are already marked [Obsolete] and scheduled for removal. Net diff: -189 LOC across the FK files. All tests pass: - PostgreSQL: 29/29 FK + 181/182 detecting_table_deltas (1 unrelated skip) - SQL Server: 22/22 FK + 31/31 detecting_table_deltas - SQLite: 24/24 FK + 361/361 full suite - Weasel.Core: 6/6 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 5: MySQL TableDelta now inherits SchemaObjectDelta<Table> MySQL.TableDelta was the only provider TableDelta still implementing ISchemaObjectDelta by hand (PG, SQL Server, Oracle, SQLite all inherit from SchemaObjectDelta<Table>). This commit brings MySQL onto the same pattern so the five deltas now compose uniformly: - Constructor forwards (expected, actual) to base - Differencing happens in the protected compare(expected, actual) override (the base ctor calls it and stores the SchemaPatchDifference on Difference); side-effect-populates Columns/Indexes/ForeignKeys/ PrimaryKeyDifference as before - WriteUpdate / WriteRollback overrides preserve the MySQL-specific DDL (MODIFY COLUMN, backtick quoting, DROP FOREIGN KEY syntax, PartitionStrategy = SchemaPatchDifference.Invalid path) - WriteRestorationOfPreviousState overridden because MySQL has historically no-op'd when Actual is null (the base default would NRE) - Expected/Actual properties drop from this file — inherited from SchemaObjectDelta<T> The public surface stays compatible: Columns / Indexes / ForeignKeys / PrimaryKeyDifference / HasChanges are still public (HasChanges is no longer required by ISchemaObjectDelta but is kept for callers that ask "anything to do?" without unpacking the Difference enum). All MySQL tests pass: 188/188 (33 integration tests against MySQL 8.0 container, 155 unit tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 7: extract DatabaseProvider memo-lookup helpers The five provider ResolveDatabaseType + ResolveXxxDbType methods were all carrying the same memo-lookup prologue: try DatabaseTypeMemo / ParameterTypeMemo; if absent and the type is Nullable<T>, look up T instead, copy the result to the nullable's slot for next-time, and return. Only the fallback after that miss differs per provider (PG → Npgsql plugin type map; SS/Oracle/MySQL → NotSupportedException; SQLite → null/Text). Move the lookup-with-nullable-promote into DatabaseProvider as two shared protected helpers — ResolveDatabaseTypeFromMemo and ResolveParameterTypeFromMemo — and rewrite each provider's two Resolve* methods as one-or-two-liners that combine the helper with the provider- specific fallback. The audit also suggested templating determineParameterType but I held back: in SS/Oracle/MySQL/SQLite the post-Resolve branches (IsEnum, IsArray, DBNull, IsConstructedGenericType) are largely unreachable because their Resolve always returns a sentinel fallback rather than null. Removing that dead code would technically preserve behaviour but risks edge cases that aren't covered by the current tests, so I left those bodies alone. Net diff: -97 LOC across the five provider files. No behaviour change. Tests pass: - PostgreSQL: 69/69 (Provider + TypeMapping + ToParameterType filter) - SQL Server: 8/8 (+8 pre-existing skips) - SQLite: 45/45 - MySQL: 37/37 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 8: prototype IDdlSyntaxStrategy on PostgreSQL + SQLite Introduces IDdlSyntaxStrategy in Weasel.Core — the pluggable strategy object the #270 plan calls out for owning provider-specific DDL syntax decisions. Step 8 deliberately covers only the parts of CREATE / DROP that genuinely differ between PostgreSQL and SQLite (the two providers at opposite ends of the feature spectrum, per the audit's "prototype against the extremes" guidance). If the shape holds for those two, step 9 will extend it across the remaining three providers when the full CREATE algorithm moves into TableBase. Strategy surface (intentionally minimal for the prototype): QuoteIdentifier(name) — provider quoting rules WriteDropTable(writer, identifier) — PG appends CASCADE, SQLite doesn't WriteCreateTableHeader(writer, id, style) — CREATE TABLE [IF NOT EXISTS] x ( InlineForeignKeyConstraints — true for SQLite (no ALTER ADD FK) AutoIncrementToken — SERIAL / AUTOINCREMENT / ... StatementTerminator — ";", or "/" for Oracle PostgresqlDdlSyntax (singleton) implements the PG side; SqliteDdlSyntax the SQLite side. Both Table classes expose Syntax => instance and route WriteDropStatement plus the CREATE-header part of WriteCreateStatement through it. The remaining CREATE body (columns / PK / inline FKs / indexes) is unchanged for now — it'll move into TableBase in step 9 once we've validated the strategy shape is workable. This is a behaviour-preserving change: the emitted SQL is byte-for-byte identical to before. Tests pass: - PostgreSQL: 185/188 (3 pre-existing concurrent-index skips) - SQLite: 361/361 (full suite) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 9: TableBase, refactor all five provider Tables Introduces Weasel.Core.TableBase<TColumn, TIndex, TForeignKey> — the cross-provider base for the five concrete Table classes that the audit predicted would absorb ~600-700 LOC of duplication. This commit lifts the high-confidence shared boilerplate; the full CREATE algorithm body is still per-provider for now (step 8's IDdlSyntaxStrategy will absorb that incrementally once the strategy shape has settled). TableBase owns: - _columns field + Columns getter (generic over TColumn : ITableColumn) - ForeignKeys + Indexes + IgnoredIndexes - MaxIdentifierLength + TruncatedNameIdentifier - ColumnFor / HasColumn / IndexFor / HasIndex / HasIgnoredIndex / IgnoreIndex - RemoveColumn (always case-insensitive — matches every provider's pre-existing impl, distinct from the per-provider NameComparison) - PrimaryKeyName with DefaultPrimaryKeyName() hook (PG/SS use pkey_{name}_{cols}, Oracle/MySQL use pk_{name}_{cols}, SQLite uses pk_{name}) - NameComparison virtual (Ordinal default — PG only; SS/Oracle/MySQL/ SQLite override to OrdinalIgnoreCase to match their pre-existing ColumnFor/HasColumn behaviour) - ToString() returning $"Table: {Identifier}" (PG/SQLite had this override; SS/Oracle/MySQL inherit it now) - ToBasicCreateTableSql() template via GetDefaultMigratorForBasicSql() hook - All ITable explicit interface implementations (AddColumn(string,string), AddColumn(string,Type), AddPrimaryKeyColumn variants, ForeignKeys, AddForeignKey) — providers supply just three abstract hooks: CreateForeignKey(name), AddColumnAndReturn(name,type), AddPrimaryKeyColumnAndReturn(name,type), and GetDatabaseTypeFor(Type). PrimaryKeyColumns stays abstract per provider — PG / SQLite store an explicit _primaryKeyColumns field, SS / Oracle / MySQL derive from Columns.Where(IsPrimaryKey). Both shapes preserved. Provider-specific items retained: - Constructor (provider wraps Identifier in its DbObjectName subclass) - WriteCreateStatement / WriteDropStatement (step 8 strategy partially consumes; rest stays per-provider until follow-up) - AllNames() override (provider-specific DbObjectName wrapping for indexes and FKs) - ColumnExpression nested class - AddColumn fluent overloads returning ColumnExpression - PrimaryKeyDeclaration() (provider-specific quoting / formatting) - Partition properties / Engine / Charset / WithoutRowId / StrictTypes / etc. SQLite's TableDelta touched _columns directly via internal access; swapped those to call the public AddColumn(TableColumn) factory which properly sets Parent and adds via the base list. IgnoredIndexes was previously a PG- and SQLite-only feature; now uniform across all providers via TableBase. Empty set on SS / Oracle / MySQL is a no-op until those providers wire it up. Diff: -398 LOC net across the five Table.cs files (-565 deletions, +167 insertions excluding the new TableBase.cs). All tests pass: - PostgreSQL: 653/656 (3 pre-existing skips) - SQL Server: 244/252 (8 pre-existing skips) - SQLite: 361/361 - MySQL: 188/188 - Weasel.Core: 6/6 - Oracle: build-only (no container available locally) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 10: canonical AutoIncrement() across providers, doc trait matrix Standardises the identity / auto-increment fluent spelling. Before 9.0 the same conceptual operation was spelled four different ways across providers: PostgreSQL: .Serial() / .BigSerial() / .SmallSerial() SQL Server: .AutoNumber() Oracle: .AutoNumber() MySQL: .AutoNumber() SQLite: .AutoIncrement() In 9.0 every provider's ColumnExpression exposes a canonical .AutoIncrement() (plus .BigAutoIncrement() / .SmallAutoIncrement() on PostgreSQL for the 64-bit and 16-bit variants). The historical names are kept as [Obsolete] aliases for one major-version cycle, with deprecation messages pointing at the canonical name. Polymorphic schema-building code now works without provider-specific casts: table.AddColumn<int>("id").AsPrimaryKey().AutoIncrement(); // works on all 5 SQL emitted is unchanged per provider — only the C# spelling on the caller side is normalised. Also adds docs/core/provider-trait-matrix.md (linked in the sidebar) that explicitly documents the per-provider differences the audit catalogued: quoting, case-folding, ALTER TABLE limitations, partitioning, JSON storage, identity spelling, primary-key name defaults, the new TableBase / ForeignKeyBase / SequenceBase / FunctionBase / ViewBase / IDdlSyntaxStrategy class hierarchy, and the constructor pattern. End users can now answer "what works on every provider vs what's provider-specific" without reading source. DocSamples updated to use the canonical AutoIncrement() spelling. Test code still uses the old names (Serial() / AutoNumber()) for now to validate the [Obsolete] forwarders work — those warnings will surface in CI as a gentle nudge to migrate but they're not failures. This completes the #270 work plan. Sequence of commits on this branch: 83cf822 JasperFx 2.0.0-alpha.8 upgrade 5ea60f3 step 1: SchemaObjectBase + SequenceBase 639628f step 2: FunctionBase 2967556 step 3: ViewBase + SQLite ViewDelta e7afae8 step 4: ForeignKeyBase (-174 LOC net) 02a6440 step 5: MySQL TableDelta ( +5 LOC net) 9c49c1e step 7: DatabaseProvider memo helpers ( -37 LOC net) 1319cf4 step 8: IDdlSyntaxStrategy prototype 99485c1 step 9: TableBase (-154 LOC net) <this> step 10: AutoIncrement + trait matrix Step 6 (TableDeltaBase shared helpers) was absorbed into step 9 since the helpers needed TableBase to exist before they could decompose cleanly. All tests pass: - PostgreSQL: 653/656 (3 pre-existing skips) - SQL Server: 244/252 (8 pre-existing skips) - SQLite: 361/361 - MySQL: 188/188 - Weasel.Core: 6/6 - Oracle: build-clean (no container locally) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Resolve #261, #265, #266 — Core.CascadeAction overloads + AOT annotations (#272) #261 — Postgres/SqlServer CascadeAction obsolete PG and SqlServer ColumnExpression.ForeignKeyTo overloads took the local [Obsolete] CascadeAction enum, with no Weasel.Core.CascadeAction overload. Users wanting to opt out of the obsolete enum had no fluent path. (Oracle and MySQL already used Core.CascadeAction directly via the using directive — verified by grep; no local CascadeAction enum exists in those projects.) Adds Core.CascadeAction sibling overloads on PG and SS ColumnExpression for ForeignKeyTo(string,...), ForeignKeyTo(Table,...), ForeignKeyTo(DbObjectName,...) and (PG only) ForeignKeyTo(PostgresqlObjectName,...). The new overloads have no defaults to avoid call-site ambiguity with the existing overloads, so .ForeignKeyTo("x", "y") binds unchanged. Callers opt into Core by passing the enum values explicitly: // before — obsolete enum .ForeignKeyTo("tbl", "id", onDelete: Weasel.Postgresql.CascadeAction.Cascade) // after — canonical .ForeignKeyTo("tbl", "id", fkName: null, onDelete: Weasel.Core.CascadeAction.Cascade, onUpdate: Weasel.Core.CascadeAction.NoAction) Defaults can be restored when the local CascadeAction enums are removed in a future major (currently scheduled per #263). #265 — AOT IL3050 on AnsiConsole.WriteException in AssertCommand Annotates AssertCommand.Execute with [RequiresDynamicCode] so the IL3050 diagnostic propagates to AOT-publishing consumers as a precise warning instead of getting swallowed inside Weasel.Core. The xmldoc explains why: db-assert is a dev-time CLI tool, so the minimum-blast-radius fix is to let consumers see the warning at the entry point rather than chase Spectre.Console's ExceptionFormatter. #266 — AOT IL2075 on CommandBuilderBase.AddParameters reflection Annotates the parameters argument of AddParameters(object) with [DynamicallyAccessedMembers(PublicProperties)] so callers carry the runtime contract through the type system, plus an [UnconditionalSuppressMessage("Trimming", "IL2075", ...)] that documents why the warning at the GetType().GetProperties() call site is expected — the trimmer's flow analysis doesn't currently propagate the DAM annotation through object.GetType(), but the runtime contract holds. xmldoc points at the longer-term source-generator path as path 2 from the issue. All existing tests pass: PG 653/656 (3 pre-existing skips), SS 244/252 (8 pre-existing), SQLite 361/361, MySQL 188/188, Core 6/6. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller
added a commit
that referenced
this pull request
May 13, 2026
* #270: Unify Table/TableColumn model + pluggable DDL syntax strategy (Weasel 9.0) (#271) * Bump JasperFx 2.0.0-alpha.1 → 2.0.0-alpha.8, JasperFx.Events → 2.0.0-alpha.3 Pulls in the latest pre-release alphas of JasperFx 2.0 ahead of #270 implementation work. Verified clean build across the solution and all non-Docker test suites pass: - Weasel.Core.Tests: 6/6 - Weasel.Sqlite.Tests: 361/361 - Weasel.CommandLine.Tests: 23/23 (with Postgres container) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 1: SchemaObjectBase + SequenceBase, refactor all 4 Sequence providers Adds two new base classes in Weasel.Core that consolidate the boilerplate every schema object reimplements today: - SchemaObjectBase: implements ISchemaObject with default AllNames() (yield Identifier), default "single COUNT(*) row" CreateDeltaAsync, and the standard FindDeltaAsync(DbConnection) pattern that builds a query command, executes it, resolves the delta, and closes the reader. ReadExistsCountAsync is a virtual hook that converts the count to long via Convert.ToInt64 — handles the int / long / decimal variation across SQL Server / MySQL / Oracle so no provider needs to override the delta logic anymore. - SequenceBase: extends SchemaObjectBase with the cross-provider sequence properties: nullable StartWith, plus Owner / OwnerColumn for providers that support OWNED BY. Refactors the 4 provider Sequence subclasses to inherit from SequenceBase: - Postgresql.Sequence: 84 → 55 LOC - SqlServer.Sequence: 86 → 55 LOC - Oracle.Sequence: 104 → 76 LOC (Oracle keeps its provider-specific OracleConnection FindDeltaAsync overload and its PL/SQL DROP wrapper) - MySql.Sequence: 65 → 58 LOC (gains the standard IInheritance, keeps its table-based emulation override) Each provider's concrete subclass now only contains the 3 things that genuinely vary per database — WriteCreateStatement, WriteDropStatement, ConfigureQueryCommand — plus a strongly-typed FindDeltaAsync overload (e.g. NpgsqlConnection) where the original surface had one. No behavioural change. All 15 Postgresql Sequence tests pass; all 6 SqlServer Sequence tests pass. Sets the foundation for FunctionBase, ViewBase, and other ISchemaObject implementations in subsequent commits. Refs #270. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 2: FunctionBase, refactor PostgreSQL + SQL Server Function Lifts the ISchemaObject boilerplate that both Function implementations were duplicating: - Constructor pattern (DbObjectName, body, optional drop statements) - IsRemoved flag with protected setter - Body(Migrator?) — routes through the provider's WriteCreateStatement - DropStatements() — returns pre-supplied drops, empty for IsRemoved, or ComputeDefaultDropStatements() (abstract per provider) - WriteDropStatement — iterates DropStatements() and writes each line - CreateDeltaAsync — reads existing function via ReadExistingFromReaderAsync (abstract per provider) and routes through CreateFunctionDelta (abstract) - AllNames, Identifier — inherited from SchemaObjectBase Each provider's concrete Function now only contains: - WriteCreateStatement (PG writes RawBody directly; SS wraps in EXEC sp_executesql) - ConfigureQueryCommand (pg_proc + pg_namespace vs. sys.sql_modules) - ReadExistingFromReaderAsync (PG reads 2 result sets; SS reads 1) - ComputeDefaultDropStatements (PG parses function signature; SS uses identifier) - GetDefaultMigrator (provider's migrator type) - ParseIdentifier / ParseSignature (PG has the signature parser; SS uses identifier-only) - FetchExistingAsync / FindDeltaAsync (strongly-typed connection overloads) - ForSql / ForRemoval static factories - PG-only: BuildTemplate / WriteTemplate (template-based function generation) Result: - Postgresql.Functions.Function: 230 → 185 LOC (-45) - SqlServer.Functions.Function: 155 → 109 LOC (-46) - Weasel.Core.FunctionBase: 0 → 133 LOC (new shared base) Net: 91 LOC removed from per-provider Function files, 133 LOC of new reusable infrastructure. All 32 Postgresql Function tests pass; all 20 SqlServer Function tests pass. Refs #270. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 3: ViewBase, refactor PostgreSQL + SQLite View, SQLite ViewDelta Add Weasel.Core/ViewBase (extends SchemaObjectBase) owning the SELECT body (ViewSql), the MoveToSchema/WithSchema template-method pair, and the ToBasicCreateViewSql helper. Both PostgreSQL.View and SQLite.View now inherit from it and only carry the provider-specific DDL. PostgreSQL View keeps its protected virtual ViewType/ViewKind/GetCreationOptions hooks so MaterializedView continues to override "VIEW"→"MATERIALIZED VIEW", 'v'→'m', and USING <access_method>. SQLite ViewDelta now inherits from SchemaObjectDelta<View> instead of hand-rolling ISchemaObjectDelta. To preserve its "no previous state to restore" no-op semantics when Actual is null, SchemaObjectDelta<T>. WriteRestorationOfPreviousState is now virtual (it was concrete and would NRE on a null Actual). No existing override conflicts — grep across src shows the new SQLite ViewDelta override is the only one. Tests pass with no behaviour change: - SQLite view tests: 38/38 (net10.0) - PostgreSQL view tests: 11/11 (net10.0) - PostgreSQL sequence + function + materialized-view tests: 47/47 - SQL Server sequence + function tests: 26/26 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 4: pull ForeignKey shared methods into ForeignKeyBase The five provider ForeignKey classes (PG, SQL Server, Oracle, MySQL, SQLite) were carrying ~120 LOC of near-identical boilerplate each. This commit consolidates the cross-provider parts into ForeignKeyBase: - Parse(string definition[, string? defaultSchema]) — fully shared; subclasses only supply ParseLinkedTable(string) to wrap the parsed table name in the provider-specific DbObjectName subclass. ParseCascadeClause helper reads ON DELETE / ON UPDATE clauses (PG keeps a `new` overload defaulting schema to "public" for catalog rows that hand back unqualified names). - LinkColumns(columnName, referencedName) — append a (dependent, principal) pair to ColumnNames/LinkedNames. MySQL's setter-driven List<string> still works correctly through the base because the setters re-marshal. - ReadReferentialActions(onDelete, onUpdate) — translates catalog cascade text via a virtual ReadAction default that handles every spelling used across the five providers (CASCADE / RESTRICT / NO_ACTION / NO ACTION / SET_NULL / SET NULL / SET_DEFAULT / SET DEFAULT). The static *Provider.ReadAction methods stay in place for external callers. - Equals / GetHashCode — structural across same-provider FKs (name + column lists + LinkedTable + both cascades). Parametrised on virtual NameComparer/ColumnComparer so case-folding providers (Oracle, SQLite, MySQL) just override the comparers. Hash drops the buggy array-reference hashing the per-provider versions had — array identity hashing gave different hashes to structurally-equal FKs, breaking HashSet/Dictionary; omitting the arrays gives more collisions but keeps semantics correct. Equality is widened from the previous strict same-type to "same provider root" — Equals(object?) walks up the inheritance chain to find the immediate subclass of ForeignKeyBase (the provider's concrete ForeignKey class) and uses IsAssignableFrom, so consumer-supplied marker subclasses (Marten ships one as `ForeignKeyTest` in PG tests) still compare equal to plain provider FKs as they did pre-9.0, while cross-provider comparison still returns false. This restores a regression a strict GetType()==GetType() check would have introduced. MisconfiguredForeignKeyException was defined identically and unused in four providers; the canonical type now lives in Weasel.Core and the provider duplicates are removed. (The PG-specific InvalidForeignKeyException and TryToCorrectForLink stay where they are.) MySQL's IsEquivalentTo is kept as a forwarder to the new EqualsCore for backward compatibility. Not consolidated (deferred): WriteAddStatement / WriteDropStatement / ToDDL all take a provider-specific Table parameter, blocked on TableBase (step 9). The obsolete OnDelete/OnUpdate shim properties stay in the subclasses since their per-provider local CascadeAction enums are already marked [Obsolete] and scheduled for removal. Net diff: -189 LOC across the FK files. All tests pass: - PostgreSQL: 29/29 FK + 181/182 detecting_table_deltas (1 unrelated skip) - SQL Server: 22/22 FK + 31/31 detecting_table_deltas - SQLite: 24/24 FK + 361/361 full suite - Weasel.Core: 6/6 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 5: MySQL TableDelta now inherits SchemaObjectDelta<Table> MySQL.TableDelta was the only provider TableDelta still implementing ISchemaObjectDelta by hand (PG, SQL Server, Oracle, SQLite all inherit from SchemaObjectDelta<Table>). This commit brings MySQL onto the same pattern so the five deltas now compose uniformly: - Constructor forwards (expected, actual) to base - Differencing happens in the protected compare(expected, actual) override (the base ctor calls it and stores the SchemaPatchDifference on Difference); side-effect-populates Columns/Indexes/ForeignKeys/ PrimaryKeyDifference as before - WriteUpdate / WriteRollback overrides preserve the MySQL-specific DDL (MODIFY COLUMN, backtick quoting, DROP FOREIGN KEY syntax, PartitionStrategy = SchemaPatchDifference.Invalid path) - WriteRestorationOfPreviousState overridden because MySQL has historically no-op'd when Actual is null (the base default would NRE) - Expected/Actual properties drop from this file — inherited from SchemaObjectDelta<T> The public surface stays compatible: Columns / Indexes / ForeignKeys / PrimaryKeyDifference / HasChanges are still public (HasChanges is no longer required by ISchemaObjectDelta but is kept for callers that ask "anything to do?" without unpacking the Difference enum). All MySQL tests pass: 188/188 (33 integration tests against MySQL 8.0 container, 155 unit tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 7: extract DatabaseProvider memo-lookup helpers The five provider ResolveDatabaseType + ResolveXxxDbType methods were all carrying the same memo-lookup prologue: try DatabaseTypeMemo / ParameterTypeMemo; if absent and the type is Nullable<T>, look up T instead, copy the result to the nullable's slot for next-time, and return. Only the fallback after that miss differs per provider (PG → Npgsql plugin type map; SS/Oracle/MySQL → NotSupportedException; SQLite → null/Text). Move the lookup-with-nullable-promote into DatabaseProvider as two shared protected helpers — ResolveDatabaseTypeFromMemo and ResolveParameterTypeFromMemo — and rewrite each provider's two Resolve* methods as one-or-two-liners that combine the helper with the provider- specific fallback. The audit also suggested templating determineParameterType but I held back: in SS/Oracle/MySQL/SQLite the post-Resolve branches (IsEnum, IsArray, DBNull, IsConstructedGenericType) are largely unreachable because their Resolve always returns a sentinel fallback rather than null. Removing that dead code would technically preserve behaviour but risks edge cases that aren't covered by the current tests, so I left those bodies alone. Net diff: -97 LOC across the five provider files. No behaviour change. Tests pass: - PostgreSQL: 69/69 (Provider + TypeMapping + ToParameterType filter) - SQL Server: 8/8 (+8 pre-existing skips) - SQLite: 45/45 - MySQL: 37/37 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 8: prototype IDdlSyntaxStrategy on PostgreSQL + SQLite Introduces IDdlSyntaxStrategy in Weasel.Core — the pluggable strategy object the #270 plan calls out for owning provider-specific DDL syntax decisions. Step 8 deliberately covers only the parts of CREATE / DROP that genuinely differ between PostgreSQL and SQLite (the two providers at opposite ends of the feature spectrum, per the audit's "prototype against the extremes" guidance). If the shape holds for those two, step 9 will extend it across the remaining three providers when the full CREATE algorithm moves into TableBase. Strategy surface (intentionally minimal for the prototype): QuoteIdentifier(name) — provider quoting rules WriteDropTable(writer, identifier) — PG appends CASCADE, SQLite doesn't WriteCreateTableHeader(writer, id, style) — CREATE TABLE [IF NOT EXISTS] x ( InlineForeignKeyConstraints — true for SQLite (no ALTER ADD FK) AutoIncrementToken — SERIAL / AUTOINCREMENT / ... StatementTerminator — ";", or "/" for Oracle PostgresqlDdlSyntax (singleton) implements the PG side; SqliteDdlSyntax the SQLite side. Both Table classes expose Syntax => instance and route WriteDropStatement plus the CREATE-header part of WriteCreateStatement through it. The remaining CREATE body (columns / PK / inline FKs / indexes) is unchanged for now — it'll move into TableBase in step 9 once we've validated the strategy shape is workable. This is a behaviour-preserving change: the emitted SQL is byte-for-byte identical to before. Tests pass: - PostgreSQL: 185/188 (3 pre-existing concurrent-index skips) - SQLite: 361/361 (full suite) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 9: TableBase, refactor all five provider Tables Introduces Weasel.Core.TableBase<TColumn, TIndex, TForeignKey> — the cross-provider base for the five concrete Table classes that the audit predicted would absorb ~600-700 LOC of duplication. This commit lifts the high-confidence shared boilerplate; the full CREATE algorithm body is still per-provider for now (step 8's IDdlSyntaxStrategy will absorb that incrementally once the strategy shape has settled). TableBase owns: - _columns field + Columns getter (generic over TColumn : ITableColumn) - ForeignKeys + Indexes + IgnoredIndexes - MaxIdentifierLength + TruncatedNameIdentifier - ColumnFor / HasColumn / IndexFor / HasIndex / HasIgnoredIndex / IgnoreIndex - RemoveColumn (always case-insensitive — matches every provider's pre-existing impl, distinct from the per-provider NameComparison) - PrimaryKeyName with DefaultPrimaryKeyName() hook (PG/SS use pkey_{name}_{cols}, Oracle/MySQL use pk_{name}_{cols}, SQLite uses pk_{name}) - NameComparison virtual (Ordinal default — PG only; SS/Oracle/MySQL/ SQLite override to OrdinalIgnoreCase to match their pre-existing ColumnFor/HasColumn behaviour) - ToString() returning $"Table: {Identifier}" (PG/SQLite had this override; SS/Oracle/MySQL inherit it now) - ToBasicCreateTableSql() template via GetDefaultMigratorForBasicSql() hook - All ITable explicit interface implementations (AddColumn(string,string), AddColumn(string,Type), AddPrimaryKeyColumn variants, ForeignKeys, AddForeignKey) — providers supply just three abstract hooks: CreateForeignKey(name), AddColumnAndReturn(name,type), AddPrimaryKeyColumnAndReturn(name,type), and GetDatabaseTypeFor(Type). PrimaryKeyColumns stays abstract per provider — PG / SQLite store an explicit _primaryKeyColumns field, SS / Oracle / MySQL derive from Columns.Where(IsPrimaryKey). Both shapes preserved. Provider-specific items retained: - Constructor (provider wraps Identifier in its DbObjectName subclass) - WriteCreateStatement / WriteDropStatement (step 8 strategy partially consumes; rest stays per-provider until follow-up) - AllNames() override (provider-specific DbObjectName wrapping for indexes and FKs) - ColumnExpression nested class - AddColumn fluent overloads returning ColumnExpression - PrimaryKeyDeclaration() (provider-specific quoting / formatting) - Partition properties / Engine / Charset / WithoutRowId / StrictTypes / etc. SQLite's TableDelta touched _columns directly via internal access; swapped those to call the public AddColumn(TableColumn) factory which properly sets Parent and adds via the base list. IgnoredIndexes was previously a PG- and SQLite-only feature; now uniform across all providers via TableBase. Empty set on SS / Oracle / MySQL is a no-op until those providers wire it up. Diff: -398 LOC net across the five Table.cs files (-565 deletions, +167 insertions excluding the new TableBase.cs). All tests pass: - PostgreSQL: 653/656 (3 pre-existing skips) - SQL Server: 244/252 (8 pre-existing skips) - SQLite: 361/361 - MySQL: 188/188 - Weasel.Core: 6/6 - Oracle: build-only (no container available locally) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * #270 step 10: canonical AutoIncrement() across providers, doc trait matrix Standardises the identity / auto-increment fluent spelling. Before 9.0 the same conceptual operation was spelled four different ways across providers: PostgreSQL: .Serial() / .BigSerial() / .SmallSerial() SQL Server: .AutoNumber() Oracle: .AutoNumber() MySQL: .AutoNumber() SQLite: .AutoIncrement() In 9.0 every provider's ColumnExpression exposes a canonical .AutoIncrement() (plus .BigAutoIncrement() / .SmallAutoIncrement() on PostgreSQL for the 64-bit and 16-bit variants). The historical names are kept as [Obsolete] aliases for one major-version cycle, with deprecation messages pointing at the canonical name. Polymorphic schema-building code now works without provider-specific casts: table.AddColumn<int>("id").AsPrimaryKey().AutoIncrement(); // works on all 5 SQL emitted is unchanged per provider — only the C# spelling on the caller side is normalised. Also adds docs/core/provider-trait-matrix.md (linked in the sidebar) that explicitly documents the per-provider differences the audit catalogued: quoting, case-folding, ALTER TABLE limitations, partitioning, JSON storage, identity spelling, primary-key name defaults, the new TableBase / ForeignKeyBase / SequenceBase / FunctionBase / ViewBase / IDdlSyntaxStrategy class hierarchy, and the constructor pattern. End users can now answer "what works on every provider vs what's provider-specific" without reading source. DocSamples updated to use the canonical AutoIncrement() spelling. Test code still uses the old names (Serial() / AutoNumber()) for now to validate the [Obsolete] forwarders work — those warnings will surface in CI as a gentle nudge to migrate but they're not failures. This completes the #270 work plan. Sequence of commits on this branch: 83cf822 JasperFx 2.0.0-alpha.8 upgrade 5ea60f3 step 1: SchemaObjectBase + SequenceBase 639628f step 2: FunctionBase 2967556 step 3: ViewBase + SQLite ViewDelta e7afae8 step 4: ForeignKeyBase (-174 LOC net) 02a6440 step 5: MySQL TableDelta ( +5 LOC net) 9c49c1e step 7: DatabaseProvider memo helpers ( -37 LOC net) 1319cf4 step 8: IDdlSyntaxStrategy prototype 99485c1 step 9: TableBase (-154 LOC net) <this> step 10: AutoIncrement + trait matrix Step 6 (TableDeltaBase shared helpers) was absorbed into step 9 since the helpers needed TableBase to exist before they could decompose cleanly. All tests pass: - PostgreSQL: 653/656 (3 pre-existing skips) - SQL Server: 244/252 (8 pre-existing skips) - SQLite: 361/361 - MySQL: 188/188 - Weasel.Core: 6/6 - Oracle: build-clean (no container locally) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Resolve #261, #265, #266 — Core.CascadeAction overloads + AOT annotations (#272) #261 — Postgres/SqlServer CascadeAction obsolete PG and SqlServer ColumnExpression.ForeignKeyTo overloads took the local [Obsolete] CascadeAction enum, with no Weasel.Core.CascadeAction overload. Users wanting to opt out of the obsolete enum had no fluent path. (Oracle and MySQL already used Core.CascadeAction directly via the using directive — verified by grep; no local CascadeAction enum exists in those projects.) Adds Core.CascadeAction sibling overloads on PG and SS ColumnExpression for ForeignKeyTo(string,...), ForeignKeyTo(Table,...), ForeignKeyTo(DbObjectName,...) and (PG only) ForeignKeyTo(PostgresqlObjectName,...). The new overloads have no defaults to avoid call-site ambiguity with the existing overloads, so .ForeignKeyTo("x", "y") binds unchanged. Callers opt into Core by passing the enum values explicitly: // before — obsolete enum .ForeignKeyTo("tbl", "id", onDelete: Weasel.Postgresql.CascadeAction.Cascade) // after — canonical .ForeignKeyTo("tbl", "id", fkName: null, onDelete: Weasel.Core.CascadeAction.Cascade, onUpdate: Weasel.Core.CascadeAction.NoAction) Defaults can be restored when the local CascadeAction enums are removed in a future major (currently scheduled per #263). #265 — AOT IL3050 on AnsiConsole.WriteException in AssertCommand Annotates AssertCommand.Execute with [RequiresDynamicCode] so the IL3050 diagnostic propagates to AOT-publishing consumers as a precise warning instead of getting swallowed inside Weasel.Core. The xmldoc explains why: db-assert is a dev-time CLI tool, so the minimum-blast-radius fix is to let consumers see the warning at the entry point rather than chase Spectre.Console's ExceptionFormatter. #266 — AOT IL2075 on CommandBuilderBase.AddParameters reflection Annotates the parameters argument of AddParameters(object) with [DynamicallyAccessedMembers(PublicProperties)] so callers carry the runtime contract through the type system, plus an [UnconditionalSuppressMessage("Trimming", "IL2075", ...)] that documents why the warning at the GetType().GetProperties() call site is expected — the trimmer's flow analysis doesn't currently propagate the DAM annotation through object.GetType(), but the runtime contract holds. xmldoc points at the longer-term source-generator path as path 2 from the issue. All existing tests pass: PG 653/656 (3 pre-existing skips), SS 244/252 (8 pre-existing), SQLite 361/361, MySQL 188/188, Core 6/6. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Add Weasel 8 → 9 migration guide New docs/migration-guide.md walks consumers through the Critter Stack 2026 upgrade path for Weasel: - Foundation pin bumps (JasperFx 2.0 alphas) + dropping net8.0 - Dedup-audit relocations (CascadeAction, BulkInsertMode, MisconfiguredForeignKeyException → Weasel.Core) with before/after snippets - Canonical AutoIncrement() fluent rename + the [Obsolete] alias table (Serial / BigSerial / SmallSerial / AutoNumber) - SchemaObjectDelta<T>.WriteRestorationOfPreviousState now virtual, including the no-op-on-null-Actual override pattern that SQLite ViewDelta uses - ForeignKey.Equals widening — restores the Marten/consumer marker-subclass pattern that strict GetType() comparison broke - Behaviour fixes worth flagging (SQL Server column casing, SQLite composite PK) - New surface (no migration impact): TableBase / ForeignKeyBase / SequenceBase / FunctionBase / ViewBase / IDdlSyntaxStrategy, with links out to the trait matrix - AOT posture summary + link to the cross-stack publishing guide - Lockstep dependency table for the 2026 wave Sidebar gets a "Migrating from 8.x to 9.0" entry under Getting Started. Drive-by fix: docs/core/provider-trait-matrix.md had a dead link to src/Weasel.Core/IDdlSyntaxStrategy.cs via a relative path VitePress can't resolve. Replaced with the GitHub URL on the 9.0 branch so the docs build passes again. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (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 #270. Tiered consolidation per the audit's recommended execution plan.
Summary
Ten commits, each independently shippable, that lift the shared
ISchemaObject/Table/ForeignKey/DatabaseProviderboilerplate out of the five provider projects intoWeasel.Core, prototypeIDdlSyntaxStrategyon PostgreSQL and SQLite, and standardise the cross-provider fluent surface.83cf8225ea60f3SchemaObjectBase+SequenceBase+ 4 Sequences639628fFunctionBase+ PG/SS Functions2967556ViewBase+ SQLiteViewDeltae7afae8ForeignKeyBaseshared methods02a6440TableDelta→SchemaObjectDelta<Table>TableDeltaBase(deferred → absorbed into step 9)9c49c1eDatabaseProvidermemo lookups1319cf4IDdlSyntaxStrategyprototype (PG + SQLite)99485c1TableBase+ refactor all 5 Tables6d92f69AutoIncrement()+ trait matrix docKey shipped APIs in
Weasel.CoreSchemaObjectBase— single-named schema object boilerplate (Identifier, AllNames, FindDeltaAsync, the standard COUNT(*) → SchemaPatchDifference template)SequenceBase,FunctionBase,ViewBase— per-family bases that own the cross-provider state (body, drop statements, view SQL, ForRemoval flag) and template the standard ISchemaObject callsForeignKeyBase—Parse(definition)withParseLinkedTablehook,LinkColumns,Equals/GetHashCodewith case-folding hook,ReadReferentialActionswith a virtualReadActiondefault that handles every spelling across all five providers' catalog textMisconfiguredForeignKeyException— canonical home (was duplicated identically in 4 providers)TableBase<TColumn, TIndex, TForeignKey>— ownsColumns/ForeignKeys/Indexes/IgnoredIndexes, navigation helpers,MaxIdentifierLength,PrimaryKeyNamewithDefaultPrimaryKeyNamehook,NameComparisonhook for case-folding providers, allITableexplicit interface implementationsIDdlSyntaxStrategy— pluggable strategy for the provider-specific DDL syntax decisions (DROP framing, CREATE header, FK inlining, auto-increment token, statement terminator). Prototype implementation on PostgreSQL + SQLite — the two providers at the extremes of feature support per the audit guidance. SS/Oracle/MySQL migrate to the strategy in a follow-up once the shape has settled.Fluent surface standardised
Every provider's
ColumnExpressionnow exposes a canonicalAutoIncrement():[Obsolete])AutoIncrement()/BigAutoIncrement()/SmallAutoIncrement()Serial()/BigSerial()/SmallSerial()AutoIncrement()AutoNumber()AutoIncrement()AutoNumber()AutoIncrement()AutoNumber()AutoIncrement()SQL emitted is unchanged per provider — only the C# spelling on the caller side is normalised. Old names kept as
[Obsolete]aliases for one major-version cycle.New documentation
docs/core/provider-trait-matrix.md(sidebar-linked) catalogues the per-provider differences end users actually care about: identifier quoting / case-folding, max-length,ALTER TABLElimitations, partitioning support, JSON storage, identity spelling, primary-key name defaults, the new class hierarchy, and the constructor pattern. Answers "what works on every provider vs what's provider-specific" without reading source.Breaking changes inventory
For Weasel 9.0 migration notes (Marten 9 / Polecat 4 / end users):
Tableclasses now derive fromTableBase<TColumn, TIndex, TForeignKey>rather than implementingITabledirectly. Code matching on: ISchemaObjectstill works.MisconfiguredForeignKeyExceptionmoves toWeasel.Core. External consumers usingWeasel.Postgresql.Tables.MisconfiguredForeignKeyException(or the SS / Oracle / SQLite equivalent) need to update theirusingstatement.Serial()/BigSerial()/SmallSerial()and SS/Oracle/MySQL'sAutoNumber()are now[Obsolete]. Migrate toAutoIncrement()/BigAutoIncrement()/SmallAutoIncrement(). Old names still work for one major-version cycle.TableDeltanow inheritsSchemaObjectDelta<Table>(was implementingISchemaObjectDeltadirectly). Code matching on the interface is fine; exact-type matches need a recompile.IgnoredIndexes(previously PG + SQLite only) is now available on every provider'sTable. Pure addition — existing code is unaffected.SchemaObjectDelta<T>.WriteRestorationOfPreviousStateis nowvirtual(was non-virtual). Backward-compatible for consumers — only enables new overrides.Test plan
🤖 Generated with Claude Code