NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
NuGet · #1946 most downloaded on NuGet
Npgsql Helpers and Postgresql Schema Migration Tool, spin off of Marten
Last release today
06 Oct 2026
Ships on a steady schedule
a new release about every 2 weeks
Some releases are documented
notes for 31 of the last 60 stable releases
1 version withdrawn
withdrawn after publishing
127 years old
233 releases · first in 1900
Nothing in this release is a breaking change. Nothing public was removed and no signature changed. One behaviour does change, deliberately — and it is…
Three unrelated things: a SQL Server advisory lock that stopped reporting locks it had lost, the other half of 9.41's Native AOT work, and table storage parameters on PostgreSQL.
Full upgrade guide: https://weasel.jasperfx.net/release-9-42
Nothing in this release is a breaking change. Nothing public was removed and no signature changed. One behaviour does change, deliberately — and it is the fix rather than a side effect.
Thanks to @NielsAudoor. If you run Polecat HotCold, or anything else built on Weasel.SqlServer.AdvisoryLock, take this release.
AdvisoryLock holds every lock as a session-scoped sp_getapplock on one connection. When SQL Server ends that session — failover, gateway reconfiguration, a killed session — it releases all of them, and HasLock went on returning true anyway. Another node then took the lock and both ran the shard. In a three-replica load test, 22,000 outbox events were processed more than once.
The root cause is one character of pattern matching, which reads as correct: _conn is not { State: ConnectionState.Closed } is true when _conn is null, because a null does not match the pattern.
What changes for callers:
HasLock now needs a live connection — a node that lost its session reports false, so the coordinator re-attains, fails, and stops the agents it no longer owns.APPLOCK_MODE monitor runs every 5 seconds while locks are held, always on and not yet configurable. The probe short-circuits when no locks are held.TryAttainLockAsync passes lockTimeoutMs: 0, so it no longer waits for a lock another node holds. This affects every caller, not only Polecat's coordinator.sp_getapplock is reentrant, and the old List<int> kept a duplicate — so a release freed one hold and left the lock held on the server while Weasel believed it was free.Thanks to @lahma for #696. A table can now declare fillfactor, the autovacuum_* family and the rest of the reloptions, and Weasel keeps them in step like any other part of the table — written on create, read back from pg_class.reloptions, and compared by the delta.
table.WithFillFactor(70)
.WithAutovacuum(vacuumScaleFactor: 0.01, insertScaleFactor: 0.02)
.WithParallelWorkers(4)
.WithStorageParameter(StorageParameterNames.VacuumTruncate, false);Only the parameters the table declares are compared. One that exists in the database but is not declared is never reset, because a DBA may have set it deliberately. Values compare numerically when both sides are numbers, so 0.05 and 0.050 do not produce a permanent false diff. For a partitioned table PostgreSQL rejects parameters on the parent, so they are written on every partition instead.
StorageParameterNames is not only convenience: StorageParameters keys are case sensitive while the DDL writer normalizes to lower case, so two spellings would render as one duplicated setting that PostgreSQL rejects with 22023. Weasel now refuses that itself, and the constants make it unreachable.
From #694. 9.41 added the Identifications factory and gave ForValueType a reflected fallback; five of the other six factories still threw under Native AOT:
System.NotSupportedException: 'Weasel.Core.Identity.SequentialGuidIdentification`1[Doc]'
is missing native code or metadata.
9.41's reasoning was that these close a generic over the document type alone, a document type is a class, and reference-type instantiations share a canonical body. That is half the rule — they share one only if ILC generated it, and nothing statically referenced those strategies. [DynamicDependency] on each factory roots the canonical body and the constructor metadata.
No API changed. If you are not publishing natively, nothing here affects you.
One column per quarter.
Nothing in this release is a breaking change. Two additions, both in Weasel.Core.Identity , both additive. Nothing public was removed and no existing…
Weasel 9.41 makes the identity runtime reachable from a consumer that holds Types rather than type
arguments — which is what a Native AOT host has to be.
Nothing in this release is a breaking change. Two additions, both in Weasel.Core.Identity, both
additive. Nothing public was removed and no existing code path changed behaviour.
Full notes: https://weasel.jasperfx.net/release-9-41
Type.MakeGenericType can close an instantiation whose type arguments are all reference types,
because those share one canonical body. It cannot close one with a value-type argument — that
needs native code ILC never generated. A consumer whose document metadata is Type-based (Marten's
ProviderGraph, Polecat's DocumentMapping) had no choice but to close Weasel's identity generics
at runtime, and an id type is routinely Guid, int or long. So a natively published store could
not be built at all.
Reflection is not a way around it, which is worth recording because it looks like one: holding the
strategy as object and invoking through MethodInfo fails differently, because GetMethods()
comes back empty — nothing statically references those members, so the trimmer took their metadata
too.
Weasel.Core.Identity.IIdentification, a non-generic facade that IIdentification<TDoc, TId>
satisfies:
public interface IIdentification
{
object Identity(object document);
object AssignIfMissing(object document, ISequenceSource sequences);
object ToRawSqlValue(object id);
Type RawSqlType { get; }
object ReadIdFromReader(DbDataReader reader, int columnOrdinal);
}If you implement IIdentification<TDoc, TId> yourself, you do not have to change anything. Every
facade member is a default implementation on the generic interface that forwards to the generic
member, so a type already compiled — including one already published and loaded against this release
by NuGet resolving Weasel up on its own — still loads.
That detail is load-bearing rather than tidy. An abstract member instead would have been a runtime
break that no amount of building or restoring reveals beforehand, which is exactly what 9.40.0
shipped and #682 caught. Verified here the only way that counts: against the published Marten
9.45.0 and Polecat 5.35.0 with this Weasel.Core resolved up, forcing the interface map the way a host
does on first load.
Every member boxes, by construction. If you can name your type arguments, keep using
IIdentification<TDoc, TId> — it allocates nothing on the path where a document already has an id.
Weasel.Core.Identity.Identifications is a non-generic entry point for all seven strategies:
ForSequentialGuid, ForRandomGuid, ForHiloInt, ForHiloLong, ForIdentityKey,
ForExternallyAssignedString and ForValueType.
Six of them close a generic over the document type alone, which is a class, so they already worked
natively. ValueTypeIdentification<TDoc, TWrapper, TInner> — strong-typed ids, Vogen /
StronglyTypedId wrappers, F# single-case DUs — did not: TInner is the wrapped primitive and
TWrapper is usually a readonly record struct, so two of three arguments are value types. The
issue predicted this rather than observing it; it is now measured, on macOS arm64 / ILC / net9.0:
IsDynamicCodeSupported = False
SequentialGuidIdentification<Doc>: constructed OK
ValueTypeIdentification<Doc,FooId,Guid>: NotSupportedException:
'Weasel.Core.Identity.ValueTypeIdentification`3[Doc,FooId,System.Guid]'
is missing native code or metadata.
Identifications.ForValueType chooses:
ReflectedValueTypeIdentification, which closes no genericThe branch is on RuntimeFeature.IsDynamicCodeSupported, a published feature switch that ILC
substitutes to false and then trims — so an AOT build neither warns about the MakeGenericType it
will never reach nor carries it. Catching NotSupportedException instead would do neither, and would
swallow real failures.
There is no mapping from (documentType, idType) to a strategy. Which strategy fits an id is a
store's policy, not a fact about the id: the same Guid is a sequential id in one configuration and
a caller-assigned one in another, and an int may be Hi-Lo or externally assigned. Marten and
Polecat do not agree, so a mapping in Weasel would be a guess wearing a library's authority. Weasel
owns the construction; the store keeps the choice.
Weasel.Core.Identity.IIdentification — the non-generic facade. Satisfied by defaultIIdentification<TDoc, TId>, so existing strategies are unaffected.Weasel.Core.Identity.Identifications — the seven For… factory methods, each building a strategyTypes.Weasel.Core.Identity.ReflectedValueTypeIdentification — the strong-typed id strategy with noForValueType under Native AOT; alsoWeasel.Core.AotSmoke now exercises Identifications.ForValueType under IsAotCompatible,TrimMode=full and the IL codes promoted to errors, so IL3050 at that call site fails CI. It wasPublishAot=true and run end to end on net9.0 and net10.0 — the analyzerNullable<wrapper> properties, ToRawSqlValue,RawSqlType, ReadIdFromReader.No breaking changes. The whole 9.39.0 → 9.40.0 public surface was audited: nothing public was removed, and the one new interface member carries a defa…
Full notes: Upgrading to 9.40
No breaking changes. The whole 9.39.0 → 9.40.0 public surface was audited: nothing public was removed, and the one new interface member carries a default implementation.
Weasel.Postgresql.AdvisoryLock.DisposeAsync released its locks one at a time, unbounded and unlogged. With session-scoped multiplexed locks plus lock monitoring, a host stop could take 60 or 120 seconds with nothing logged at all — Medallion's connection monitor parks a connection in a one-minute wait, and the last release on it fires no wake-up because the handle drops its monitoring registration before running pg_advisory_unlock.
Releases now run concurrently and the wait is bounded by the new AdvisoryLockOptions.ReleaseTimeout (default 5s), with any overrun logged.
This changes what DisposeAsync waits for — it now returns after the timeout with outstanding releases backgrounded. Nothing leaks, and it is strictly better than the old stall, but set ReleaseTimeout to Timeout.InfiniteTimeSpan (or any non-positive value) to opt out.
#677 · #680 · #681 · #685 · #686 · #687
The migration path has deferred foreign keys to tables created later for a long time. The script path wrote objects in yield order, so the same model could migrate cleanly and emit a script that failed on its first constraint. ToDatabaseScript() and WriteScriptsByTypeAsync() now order objects and features by dependency — features too, because WriteScriptsByTypeAsync puts each in its own file, so there the feature order is the script order.
A second run then failed on the foreign key itself. Measured on all six providers; three were already right:
| PostgreSQL | now catches duplicate_object |
| Oracle | now checks all_constraints |
| MySQL | now declares the key inline in CREATE TABLE |
| SQL Server, Firebird, SQLite | already guarded |
MySQL's rendered CREATE TABLE changes shape: foreign keys move inside the parentheses and no trailing ALTER TABLE … ADD CONSTRAINT is emitted. Identical schema, delta detection unaffected — but if you assert on that text, it moved.
All creation-path only. Every WriteAddStatement is untouched, so no rendered migration changes.
ICommandBuilder.ParameterCountHow many parameters the command being built already carries, so a caller rendering a value list can decide against Migrator.MaxParametersPerCommand instead of guessing. The budget belongs to the command, not the fragment, so a per-fragment threshold does not close the hole.
If you implement ICommandBuilder, you need change nothing. It has a default implementation returning ICommandBuilder.UnknownParameterCount (-1). Without that default this would have been a runtime break — the CLR refuses the type the first time a host loads it, which no build or restore reveals.
AdvisoryLockOptions.ReleaseTimeoutICommandBuilder.ParameterCount / UnknownParameterCountWeasel.Core.Migrations.ISchemaObjectWithDependencies — declare a creation-order dependency that is not a foreign keyWeasel.Core.Migrations.SchemaObjectOrdering.InDependencyOrderForeignKey.WriteGuardedAddStatement (PostgreSQL, Oracle), ForeignKey.WriteInlineDefinition / ToInlineDefinition (MySQL)An eighth provider — Weasel.Firebird , for Firebird 3, 4 and 5 — and a SQL Server migration fix for anyone who has seen Invalid column name out of an
An eighth provider — Weasel.Firebird, for Firebird 3, 4 and 5 — and a SQL Server migration fix for anyone who has seen Invalid column name out of an apply.
Full notes: https://weasel.jasperfx.net/release-9-39
#668, reported by @peejayess through Wolverine.
SQL Server compiles a whole batch before running any of it and binds column names at compile time, so a CREATE INDEX … WHERE ([expires] IS NOT NULL) in the same batch as the ALTER TABLE … ADD expires failed with error 207 — and the ALTER never ran either.
The line is expression versus name list, not indexes: a filtered index's predicate, a check constraint and a computed column all failed, while an index's key columns, its INCLUDE list and a foreign key's columns always worked. A plain index on a new column was fine, which is what hid this. Two of the three failing cases were beyond the original report.
#670.
WriteRollback had no check-constraint handling at all, so a rolled-back migration left an added constraint in place and a replaced expression permanently replaced. If you have rolled one back, the table is not what your model says and no delta will tell you — an undeclared actual constraint is filtered out of the comparison. Read sys.check_constraints.
#666, contributed by @lahma — Firebird 3, 4 and 5 at parity with Weasel.MySql, with EF Core support and CI on all three majors.
dotnet add package Weasel.FirebirdTwo new Core hooks came with it, both virtual with the previous behaviour as the default: Migrator.FingerprintTableName and Migrator.OrderRollbacks.
One change in rendered output: a SQL Server table delta that changes a column now carries GO batch separators. Weasel's executors and sqlcmd both split on them, so the normal path is unaffected. If you hand TableDelta.WriteUpdate output to a single SqlCommand yourself, split it first with SqlServerBatchSplitter.Split(sql) — the same adjustment stored procedure DDL needed in 9.35.
Coming from 9.37 or earlier? 9.38.0 shipped to NuGet without notes; its page is now written at https://weasel.jasperfx.net/release-9-38 and it carries a deliberate change to what an Oracle migration reports about identity columns.
One fix, on Oracle, in three parts: an identity column could not be created, was never read back, and made its schema undroppable.
One fix, on Oracle, in three parts: an identity column could not be created, was never read back, and made its schema undroppable.
Full notes: https://weasel.jasperfx.net/release-9-38
Released after the fact. 9.38.0 went to NuGet on 2026-10-04 without a tag, a GitHub release or a notes page. The packages are real and unchanged — this release and the
9.38.0tag are the paperwork catching up, and nothing here is new behaviour since then.
#667.
GENERATED BY DEFAULT AS IDENTITY is its own clause rather than part of the column type, and neither the DDL side nor the read side accounted for that:
ORA-03076. The identity clause stands in the DEFAULT slot; they are alternatives, not additions. So AutoIncrement() could not create a table at all.ColumnSql did not select IDENTITY_COLUMN and ReadColumn never set IsAutoNumber, so every column came back non-identity.ISEQ$$_… sequence behind the column, which Oracle refuses with ORA-32794. One identity column anywhere in the schema was enough to make the schema undroppable.IsAutoNumber now counts towards TableColumn equality, so adding or dropping identity on an existing Oracle column is finally reported as a difference. That is only safe together with the reader fix — comparing a flag that is never read back would make every identity column drift forever.
If you worked around the broken AutoIncrement() by writing the identity clause into a column's type string, drop that workaround and call AutoIncrement(). A type string carrying the clause can never match what the catalog reports back, so the column keeps drifting.
Five fixes. Full upgrade notes .
Five fixes. Full upgrade notes.
A JSON index is in sys.indexes, but its sys.index_columns row carries key_ordinal = 0, which the column query filtered out. The index came back with no columns, matched nothing declared, landed in Indexes.Extras, and was emitted as a DROP INDEX — so every apply silently removed it. JSON indexes are now read through sys.json_indexes / sys.json_index_paths, behind an object_id check so nothing changes on 2022 or earlier. (#661)
The same change adds JsonIndexDefinition, so CREATE JSON INDEX can be declared, rendered, compared and asserted on like any other schema object.
Split commands dropped InitialLONGFetchSize, so ALL_IND_EXPRESSIONS.COLUMN_EXPRESSION and ALL_VIEWS.TEXT — both LONGs — read back empty. PRIORITY DESC came back as its hidden SYS_NC00007$ column and every apply rebuilt the index. It failed only in combination, which is why per-object tests never caught it. Contributed by @lahma, found through Quartz.NET's Weasel integration. (#660)
TableDelta.HasChanges() threw a NullReferenceException for a table that needs creating — the one case whose answer is unambiguously "yes". It reached operators through Wolverine's resource check, which reports any exception as "Missing known broker resources". SQL Server, PostgreSQL and SQLite. (#658)
The lock was released only on the success paths, so a migration that threw left it held and locked every other replica out of migrating — while the node that stranded it reported nothing. Disposing the connection was not enough: Npgsql resets a pooled connection when it is next used, not when it is returned, so the session lock outlived the apply for as long as that connection sat idle. (#659)
It replaced conn without disposing what it overwrote, leaving a live connection per attempt for the finalizer to find — against a server that had just refused or terminated one. (#664)
Minor rather than patch because #661 adds public API and two of these change migration output — the same reason 9.29.1 was renumbered to 9.30.0.
Verified across both target frameworks and all eight suites against PostgreSQL 17, SQL Server 2022, MySQL 8.0.46, Oracle Free 23 and SQLite: net9.0 3984 passed, net10.0 3980 passed, 0 failed.
A drift release, and one new diagnostic.
A drift release, and one new diagnostic.
Most of 9.36.0 is a single shape of defect found five times over: a model declares something in a spelling the server rewrites when it stores it, so the column or index never matches itself, every migration re-issues DDL that changes nothing, and the delta never settles. MySQL type synonyms, Oracle ANSI type names, Oracle and MySQL index key direction, Oracle index tablespaces and SQL Server CAST all did this.
Oracle: a table with a string column default could not be created at all under the default CreateIfNotExists style — the quote ended the EXECUTE IMMEDIATE literal and the create failed with PLS-00103 (#643).
Two migrations change behaviour. Changing a computed column's definition now drops and recreates the indexes and foreign keys that depend on it (#638). That migration used to fail outright on SQL Server, and used to succeed while silently dropping the index on PostgreSQL.
Every drift fix was checked in both directions. The spellings that already reconciled still reconcile, and a genuinely different type, length or direction is still reported as drift. No settled table acquires a new ALTER.
JasperFx 2.76.0 is the new floor, the first release carrying the IAdvisoryLock.FindHolderAsync contract.
#650. IAdvisoryLock could acquire a distribution lock but not say who holds it — HasLock(lockId) answers only "does this node". So when a projection agent stopped with ProgressionProgressOutOfOrderException, meaning two processes believed they owned the same shard, nothing could name the other one. Under Marten HotCold and Polecat the lock is the authority.
var holder = await advisoryLock.FindHolderAsync(lockId, token);
// null means unheld; IsCurrentNode == false means someone else has itBoth reads were measured against live servers rather than inferred, and one measurement changed the implementation: pg_locks stores the advisory key as two 32-bit halves, so a negative lock id sign-extends into classid = 0xFFFFFFFF. The obvious query reports a held lock as unheld — precisely the reassuring answer an operator chasing a double-runner must never be handed.
| Area | Issues |
|---|---|
Oracle — respelled column types (every TimeSpan drifted), index direction, index tablespace, guarded-DDL quotes |
#649 #645 #644 #643 |
MySQL — type synonyms, index direction, IgnoreIndex honoured, CREATE DATABASE only when missing |
#646 #641 #642 #647 |
SQLite — add-only rebuilds keep undeclared objects, rollbacks keep rows, PreserveIdentifierCase, sqlite_sequence |
#639 #648 #640 #546 |
SQL Server — CAST reconciles against the stored CONVERT; computed-column dependants |
#637 #638 |
| Contributing — one npm dependency tree for the docs | #582 |
Two SQLite fixes are the default for any EF Core model, since MapToTable sets both AddOnlyMigrations and PreserveIdentifierCase: a rebuild no longer drops the columns the model does not declare (and their rows), and a PreserveIdentifierCase table no longer renames every column to itself on every single migration.
#546 had been open since 9.29.0, where it shipped as a known issue.
IndexDefinition.DescendingColumns on MySQL and Oracle, matching SQL Server'sMigrator.GenerateDeleteAllSqlAsync, a connection-aware overload defaulting to the existing behaviourIAdvisoryLock.FindHolderAsync implemented on PostgreSQL and SQL ServerThanks to @lahma for eleven of the seventeen commits.
One fix, PostgreSQL only: #634 .
One fix, PostgreSQL only: #634.
SchemaUtils.DropSchema reported success when every retry failedIt retried the drop up to three times and, on the third failure, returned as though it had succeeded. One condition served two opposite outcomes:
if (success || ++reconnectionCount == maxReconnectionCount)
return;so the throw below the loop was unreachable, and the loop's own reconnectionCount < maxReconnectionCount could never be false either.
dropSchema reports failure for exactly one condition — 57P01 admin_shutdown — and rethrows everything else. So the only way to exhaust the attempts is a server still down after the backoff, which is precisely the case the caller needs to hear about. It heard nothing, and carried on as though the schema were gone.
An exhausted retry now throws, which is what the unreachable line always intended:
System.InvalidOperationException: Unable to drop schema: my_schema
Only if a drop was already failing silently.
admin_shutdown still propagates unretried and unwrapped.try/catch around this call that never fired, it may start firing — and what it is telling you is that the drop was not happening.No signature changed: the public two-argument method drops its own async keyword and delegates to a new internal overload taking the single attempt as a delegate, which is source- and binary-compatible. That overload is what makes the exhausted path testable without a PostgreSQL server that stays down across three tries — the reason the defect went uncovered in the first place.
The EF Core set: #627 , #628 and #629 .
The EF Core set: #627, #628 and #629.
ComplexProperty with a Weasel-managed EF Core migrationCheck those tables on your current version first — this is a bug that has already run.
A table-split ComplexProperty (one without ToJson()) was not mapped at all, on 9.35.0 and every earlier version carrying the EF bridge. Against a table EF Core itself created, a CreateOrUpdate migration emitted drop column total_amount. With PascalCase column names that failed with 42703 and aborted the migration, which is how this was found. With lowercase column names — what UseSnakeCaseNamingConvention() or an explicit HasColumnName produces — it succeeded, and the data is gone.
On a fresh database, the same gap created the table without those columns, and the first EF insert failed with 42703.
Every Weasel-managed EF migration path runs through this mapper, including Wolverine's UseEntityFrameworkCoreWolverineManagedMigrations() and its tenanted DbContext builders.
A migration for an EF-derived table no longer drops columns, indexes or foreign keys that the EF model does not declare. That is the fix for #629 — the mapper has known translation gaps (TPC, entity splitting, temporal period columns, Npgsql enums), and reading an untranslated column as a removed one made every one of them a data-loss branch.
If you relied on a removed EF property taking its column with it, set EfSchemaMappingCustomization.AllowDrops = true. Tables you define in Weasel directly are unaffected.
42703 against a quoted PascalCase column (#627).ToJson() container column is no longer typed jsonb on every provider — SQL Server gets nvarchar(max), MySQL and SQLite TEXT, Oracle CLOB (Migrator.DefaultJsonColumnType).ITable.AddOnlyMigrations, EfSchemaMappingCustomization.AllowDrops, ISchemaObjectDeltaWithWithheldDrops, IMigrationLogger.WithheldDrop, AddOnlyMigration, Migrator.DefaultJsonColumnType. All described in the upgrade guide.
⚠️ Breaking change, SQL Server only
Full upgrade guide: https://weasel.jasperfx.net/release-9-35
Two independent fixes, plus CI hygiene.
#593 — PRs #620, #624, thanks to @jovball
A rendered migration script (db-patch, db-dump, WriteMigrationFileAsync, ToDatabaseScript) now runs as one file under sqlcmd or SSMS, and runs a second time against the same database without failing.
CREATE OR ALTER PROCEDURE between GO lines, because T-SQL requires a procedure definition to be the first statement of its batchDROP INDEX IF EXISTSSET QUOTED_IDENTIFIER ON;, so no sqlcmd flags are neededGO is refused at render time — neither sqlcmd nor Weasel's splitter parses string literals, so such a line would split the batch inside the definitionDROP INDEX IF EXISTS and CREATE OR ALTER make SQL Server 2016 SP1 the effective floor.
Rendered stored procedure DDL now carries GO lines. A consumer that executes that text through its own SqlCommand must split it first with the new SqlServerBatchSplitter.Split, or switch to CreateAsync. Everything Weasel executes itself already splits.
#621 — PRs #622, #625, reported and fixed by @ayuksekkaya
BatchedQuery.Query<T>() and QuerySingle<T>() built entities by reflecting over the entity type's scalar properties. Anything that was not flat came back incomplete and nothing reported an error: owned types, owned JSON columns, complex properties and complex collections left null or empty, Included navigations empty, a collection Include returning one parent per child row, and nothing tracked. Saving one of those entities wrote the empty members to the database.
EF Core now runs every batched query and materializes every result, so a batched query returns exactly what the same query returns on its own — tracking, identity resolution, owned/complex/JSON members, Includes and projections.
Batching into a single round trip requires the new BatchedQueryInterceptor:
services.AddDbContext<MyDbContext>(opts =>
{
opts.UseNpgsql(connectionString);
opts.UseWeaselBatchedQueries();
});Without it — and where a batch cannot faithfully stand in for separate execution — every queued query runs on its own round trip with the same results. Correctness does not depend on registering anything; only the round trip count does.
Wolverine users: Wolverine batches query plans automatically as soon as a handler has two of them against the same DbContext, so this defect could appear in a working handler when an unrelated second plan was added. Wolverine registers the interceptor from the release that takes this version of Weasel — JasperFx/wolverine#4626.
#611 — PR #623. The seven CI workflows have concurrency groups, so a re-pushed branch stops stacking 18 jobs behind the ones it supersedes.
SqlServerBatchSplitter.Split(string) — sqlcmd's GO semanticsMigrator.SplitIntoBatches(string) — public virtual, returns the whole string by default; every provider but SQL Server keeps thatIndexDefinition.WriteCreateStatement(Table, TextWriter) — the guarded emission methodBatchQueryExtensions.UseWeaselBatchedQueries(...) and BatchedQueryInterceptorEvery PR was 18/18 green, and master was packed locally and run against both downstream consumers before this release: Wolverine.SqlServerTests 483/0, Wolverine.EfCoreTests 233/0, Polecat.Tests 2809/0, Weasel.SqlServer.Tests 642/0, Weasel.EntityFrameworkCore.Tests 130/0.
Full upgrade guide: https://weasel.jasperfx.net/release-9-34
Full upgrade guide: https://weasel.jasperfx.net/release-9-34
9.33.0 removed four public SharedLockExtensions signatures. Anything compiled against 9.32.0 or earlier — current WolverineFx.SqlServer among them — throws MissingMethodException at runtime once 9.33.0 is resolved underneath it. An application on Polecat 5.31.0 (which floors 9.33.0) plus current WolverineFx fails the moment a host boots and takes the migration lock.
Nothing catches it before runtime: restore is clean because 9.33.0 satisfies the declared 9.32.0 floor, compile is clean because the caller is already compiled, and Weasel.Postgresql was untouched — so a green Marten/Postgres suite says nothing about it.
9.34.0 restores the four signatures. No source change is needed in either direction, and lockTimeoutMs keeps working as 9.33.0 introduced it.
db-ef-migration translated every index into a CreateIndexOperation, whose Columns is a list of identifiers the provider quotes one at a time. That broke two ways:
Columns, so the migration died on apply with 42703: column "(data ->> 'Kind')" does not exist.CreateIndexOperation has nowhere to put an operator class, so a GinIndexJsonData() index applied without error using the default jsonb_ops instead of the declared jsonb_path_ops. A different index from the one declared, on a migration that reported success. The differ compared the same incomplete subset, so later changes to those options produced no migration at all.Such an index is now emitted as its own CREATE INDEX, and diffed on that DDL. Only the index takes that route — the table around it stays typed, so snapshot diffing keeps working for Marten document tables. The previous workaround (ForceRawSql over the whole table) made every subsequent change to it something the differ refused.
Your first
db-ef-migration addafter upgrading may contain index changes you did not make. That is the correction landing — those indexes really are wrong in the database — but review it rather than assuming it is spurious. See the upgrade guide for the detail, including the oneDown()edge case.
Reported with a complete repro by @Jaxter.
ITableIndex gains two membersHasProviderSpecificOptions and ToDDL(ITable), implemented by all five providers. A source break only if you implement ITableIndex yourself, which is unusual.
An exception sweep. Eight issues ( #595 – #602 ) about what Weasel says when something goes wrong, plus JasperFx 2.74.0. No migration behaves differen
An exception sweep. Eight issues (#595–#602) about what Weasel says when something goes wrong, plus JasperFx 2.74.0. No migration behaves differently, and no DDL changes.
Source-compatible, and all three keep the original exception as InnerException — but a catch written against the old type stops matching.
| Failure | Was | Now |
|---|---|---|
| A role lacking privilege during a migration | the raw PostgresException / SqlException / MySqlException / OracleException |
InsufficientDatabasePrivilegeException |
sp_getapplock refusing the SQL Server migration lock |
a bare System.Exception |
GlobalLockUnavailableException |
AssertDatabaseMatchesConfigurationAsync on drift that cannot be applied incrementally |
SchemaMigrationException |
DatabaseValidationException |
If you match on a SQLSTATE — catch (PostgresException e) when (e.SqlState == "42501") is the common one — read the upgrade guide first.
InsufficientDatabasePrivilegeException names the role, the database, the refused statement, and the two remedies. Covers PostgreSQL, SQL Server, MySQL and Oracle. Introspection being privilege-filtered — which makes a restricted role look at an empty database — is now called out by db-assert, and the docs gain a Permissions section.AutoCreate.All's drop-and-recreate is the only branch in the migrator that destroys data, and nothing announced it. It now warns once per object before any statement runs, naming why the change could not be applied in place. New Migrator.RefuseDestructiveChanges (default off) refuses the branch outright.sp_getapplock return codes are decoded (#599), the lock timeout is configurable at last, and SQL Server gains an IGlobalLock implementation so ResourceMigrationFailureMode.ContinueOnFailures means the same thing on both providers.ConcurrencyException always carries the UpdateRevision hint (#595) — one throw site, so this fixes Marten, Polecat and Fisher at once.StreamAction in hand instead of parsing redacted provider error detail.AutoCreate documentation corrected (#597) — None does not throw, and CreateOrUpdate does drop columns the model no longer declares.Nothing published for this version
Just supporting some downstream development
Just supporting some downstream development
Full Changelog: 9.31.1...v9.31.2
One fix, and the release notes the 9.31 line never got.
One fix, and the release notes the 9.31 line never got.
#584, closing #583, raised from JasperFx/marten#5364.
ManagedListPartitions kept its tenant registry in a plain Dictionary that it mutated in place, and published it two ways that both hand out the live instance — Partitions returns a ReadOnlyDictionary, which wraps rather than copies, and IListPartitionManager.Partitions() is a lazy iterator over the same dictionary, the one ListPartitioning.Partitions() calls for DDL and delta calculation. InitializeAsync then Clear()ed and refilled that instance.
One manager instance is shared across every database in a store, so db-apply --parallel N over more than N databases has a late-starting worker reloading the registry while an earlier worker is still enumerating it. That surfaced as Collection was modified; enumeration operation may not execute out of DatabaseBase.assertValidIdentifiers — reported as 1 failure in 29 databases at --parallel 16.
The quieter failure is the one worth upgrading for. Clear()-then-refill also lets a reader observe an empty or partial partition set and generate a script, or compute a partition delta, against it, with nothing raised at all.
Both managers now publish an immutable snapshot and swap it copy-on-write. Weasel.SqlServer's ManagedTenantPartitions had the identical shape and gets the same treatment, which matters for Polecat.
This is latent rather than a recent regression — it is not caused by anything in 9.31.0.
9.31.0 shipped to NuGet without notes. The 9.31 upgrade guide now covers it, including two silent runtime behaviour changes that went out undocumented:
ChangeTracker DeepEquals fallback is opt-in now (#578) — semantic JSON change detection is off unless you set IStorageSession.UseSemanticJsonChangeDetection. It matters if your documents carry dictionaries or sets that get rebuilt.5 on a row that does not exist yet now lands the column at -5.Also in 9.31.0: JSON column reads stream instead of materializing a UTF-16 string (#576 — 20% fewer allocated bytes, Gen2 collections halved at 120 KB), a never-created stream is at version 0 rather than a conflict (#580), and a run of internals lifted out of Marten, Polecat and Fisher into Weasel.Storage and Weasel.Core.
Full notes: https://weasel.jasperfx.net/release-9-31
Nothing published for this version
The same code as 9.29.1, under the version it should have had. git diff 9.29.1 9.30.0 -- src/ is empty.
The same code as 9.29.1, under the version it should have had. git diff 9.29.1 9.30.0 -- src/ is empty.
9.29.1 shipped a change that alters what an existing database does on its next migration. A patch bump is not where anyone looks for that, so it is republished here behind a minor version. 9.29.1 is not broken and stays on nuget.org — it is numbered wrong, and 9.30.0 is the one to take. If you are already on 9.29.1 there is nothing to do but change the number; the assemblies are identical.
Character column widths are compared now. On MySQL, SQL Server and Oracle, a varchar whose width differs between your model and the database produces an ALTER where it previously produced nothing — in both directions. A model narrower than an existing column now emits a narrowing ALTER, which can fail on real data. That is the correct contract, and it is what makes the downstream fix work, but it is worth checking your models against a production catalog before the first migration on 9.30.0.
PostgreSQL and SQLite are deliberately untouched by that change.
#550 — a widened character column is detected. For JasperFx/wolverine#4246. TableColumn.RawType() strips the parenthesised part of a type before comparing, on MySQL, SQL Server and Oracle alike. For most types that is right — MySQL 8 reports a bare INT for a column declared int(11), a DECIMAL carries a precision and a scale — and comparing those wholesale drifts on every schema check for tables nobody touched. Character and binary lengths are the exception: declared by the model, reported faithfully by every catalog, and load-bearing, because a column narrower than the value being written fails the insert. Widening a varchar in a model was invisible to the differ, so an existing database kept the old width forever.
A length is compared only for the types whose single parenthesised argument really is a character or byte count (CHAR, VARCHAR, NCHAR, NVARCHAR, CHARACTER, VARCHAR2, NVARCHAR2, BINARY, VARBINARY, RAW), and only when both sides state one — "cannot tell" never reports drift, so a model that declares a bare type keeps comparing the way it always has. varchar(max) is an unbounded sentinel, and Oracle's VARCHAR2(100 CHAR) and (100 BYTE) both read as 100.
Two Oracle defects in AlterColumnTypeSql surfaced with it, neither reachable before because a column type delta was almost never detected in the first place. It emitted the shape the column was moving from where the SQL Server and PostgreSQL twins emit the one it is moving to, so the statement altered the column to what it already was; and it restated the column's nullability, which Oracle rejects when it is not changing (ORA-01451, ORA-01442).
#548 — a view body is compared without normalizing away its string literals. SQL Server and SQLite normalized a view body by stripping every whitespace character and folding case across the whole string, literals included. Two views whose only difference was inside a literal compared equal: changing where name = 'active' to 'ACTIVE', or 'a b' to 'ab', both reported the schema as in sync. The view kept matching the old rows and the migration never ran.
Normalization now runs a scanner — Weasel.Core.ViewSqlNormalizer — that folds whitespace and case only outside string literals and copies literal contents verbatim. Delimited identifiers and comments are scanned as themselves, so the apostrophe in [Customer's Name], "o'brien" or -- don't cannot open a literal and invert inside/outside for the rest of the body. Everything outside a literal is behaviour-neutral, so no existing schema starts reporting a spurious migration.
#549 — a SQLite rebuild no longer mutates the model it was given. writeTableRecreation and its rollback twin built the throwaway replacement table out of the caller's own TableColumn instances, and Table.AddColumn(TableColumn) sets column.Parent = this — so generating a migration silently reparented a long-lived model's columns onto a temp table discarded a few lines later. No rebuild Weasel emits is affected; the damage lands afterwards, on the model that outlives the migration. Set StrictTypes on it and its columns consult the discarded parent, so the next diff reports drift on a column that already matches and the CREATE fails with unknown datatype for .... Each column is cloned instead, and the rebuild DDL is unchanged.
#546 is unchanged: on SQLite, resetting identity against a schema with no AUTOINCREMENT table fails with no such table: <schema>.sqlite_sequence.
Three fixes. Two are silent false negatives — a schema that had drifted reported itself in sync — and the third is a model-corruption bug in the SQLit
Three fixes. Two are silent false negatives — a schema that had drifted reported itself in sync — and the third is a model-corruption bug in the SQLite rebuild.
Character column widths are compared now. On MySQL, SQL Server and Oracle, a varchar whose width differs between your model and the database produces an ALTER where it previously produced nothing — in both directions. A model narrower than an existing column now emits a narrowing ALTER, which can fail on real data. That is the correct contract, and it is what makes the downstream fix work, but it is worth checking your models against a production catalog before the first migration on 9.29.1.
PostgreSQL and SQLite are deliberately untouched by that change.
#550 — a widened character column is detected. For JasperFx/wolverine#4246. TableColumn.RawType() strips the parenthesised part of a type before comparing, on MySQL, SQL Server and Oracle alike. For most types that is right — MySQL 8 reports a bare INT for a column declared int(11), a DECIMAL carries a precision and a scale — and comparing those wholesale drifts on every schema check for tables nobody touched. Character and binary lengths are the exception: declared by the model, reported faithfully by every catalog, and load-bearing, because a column narrower than the value being written fails the insert. Widening a varchar in a model was invisible to the differ, so an existing database kept the old width forever.
A length is compared only for the types whose single parenthesised argument really is a character or byte count (CHAR, VARCHAR, NCHAR, NVARCHAR, CHARACTER, VARCHAR2, NVARCHAR2, BINARY, VARBINARY, RAW), and only when both sides state one — "cannot tell" never reports drift, so a model that declares a bare type keeps comparing the way it always has. varchar(max) is an unbounded sentinel, and Oracle's VARCHAR2(100 CHAR) and (100 BYTE) both read as 100.
Two Oracle defects in AlterColumnTypeSql surfaced with it, neither reachable before because a column type delta was almost never detected in the first place. It emitted the shape the column was moving from where the SQL Server and PostgreSQL twins emit the one it is moving to, so the statement altered the column to what it already was; and it restated the column's nullability, which Oracle rejects when it is not changing (ORA-01451, ORA-01442).
#548 — a view body is compared without normalizing away its string literals. SQL Server and SQLite normalized a view body by stripping every whitespace character and folding case across the whole string, literals included. Two views whose only difference was inside a literal compared equal: changing where name = 'active' to 'ACTIVE', or 'a b' to 'ab', both reported the schema as in sync. The view kept matching the old rows and the migration never ran.
Normalization now runs a scanner — Weasel.Core.ViewSqlNormalizer — that folds whitespace and case only outside string literals and copies literal contents verbatim. Delimited identifiers and comments are scanned as themselves, so the apostrophe in [Customer's Name], "o'brien" or -- don't cannot open a literal and invert inside/outside for the rest of the body. Everything outside a literal is behaviour-neutral, so no existing schema starts reporting a spurious migration.
#549 — a SQLite rebuild no longer mutates the model it was given. writeTableRecreation and its rollback twin built the throwaway replacement table out of the caller's own TableColumn instances, and Table.AddColumn(TableColumn) sets column.Parent = this — so generating a migration silently reparented a long-lived model's columns onto a temp table discarded a few lines later. No rebuild Weasel emits is affected; the damage lands afterwards, on the model that outlives the migration. Set StrictTypes on it and its columns consult the discarded parent, so the next diff reports drift on a column that already matches and the CREATE fails with unknown datatype for .... Each column is cloned instead, and the rebuild DDL is unchanged.
#546 is unchanged: on SQLite, resetting identity against a schema with no AUTOINCREMENT table fails with no such table: <schema>.sqlite_sequence.
Clears 9.28's known upgrade break, makes the SQLite table rebuild safe against a database that has foreign keys, and adds two capabilities. Minor rath
Clears 9.28's known upgrade break, makes the SQLite table rebuild safe against a database that has foreign keys, and adds two capabilities. Minor rather than patch: two of these add API, and two change what an existing migration does.
You can upgrade straight to 9.29. The 9.28 break — a numeric or decimal SQLite column making the first migration throw on every AutoCreate except All — is fixed. No pass through 9.28, no run with AutoCreate.All.
Two changes can turn a migration that used to appear to succeed into one that throws. Both are deliberate, and in both cases it was not succeeding before.
A SQLite rebuild that would leave a dangling foreign key reference is now refused and rolled back, with an InvalidOperationException naming the table. Previously it committed the damage. The check only runs when foreign key enforcement was on to begin with — a database deliberately running with foreign_keys OFF is allowed to hold dangling rows and is not second-guessed.
A custom IMigrationLogger that declines to rethrow used to let a failed rebuild reach COMMIT half-applied. It is still handed the failure through OnFailure, but the migration no longer continues past it.
#542 — the guard in front of a rebuild no longer refuses it. Closes #538. SchemaMigration.AssertPatchingIsValid threw for SchemaPatchDifference.Invalid on every AutoCreate except All, without asking whether the delta could actually apply the change. Both apply paths below it do ask, and answer a rebuildable delta by rebuilding it and copying the data rather than dropping and recreating. The gate was stricter than the thing it gated. CreateOrUpdate now permits a rebuild; CreateOnly still refuses one, and needs no special case to do it — the delta is still Invalid, so the migration's Difference is still not Create.
This reverses a decision #477 shipped on purpose, that a rebuild always needs AutoCreate.All because a rebuild which also drops a column takes that column's data with it. That reasoning did not survive contact with the code: AssertPatchingIsValid could only express it by refusing every rebuildable delta, including the great majority that drop nothing, while SQLite's ordinary Update path was already emitting ALTER TABLE … DROP COLUMN under CreateOrUpdate. The strict rule only refused the same loss when the column happened to sit in a key.
#539 — the SQLite table rebuild is atomic. SQLite cannot ALTER most of a table, so a change is applied by rebuilding it: create, copy, drop, rename. Those four statements ran with no transaction, each one autocommitting. Rebuilding a table that another table's foreign key referenced broke the database — the DROP failed on its implicit delete, after the replacement had been created, filled and committed. The orphan _new table survived, CREATE TABLE IF NOT EXISTS no-opped on it at the next start, and every run after that failed differently until someone dropped it by hand. Foreign keys are on by default in Microsoft.Data.Sqlite and in all three SqlitePragmaSettings presets, so this was the ordinary case. The rebuild now runs the way SQLite documents it: enforcement suspended, the whole thing in one transaction, foreign_key_check before the commit. Two smaller fixes ride along — an AUTOINCREMENT table came out of a rebuild with a lower sqlite_sequence high-water mark than it went in with and reissued an id it had already handed out, and a view over the rebuilt table failed the rename outright.
#540 — mutually referencing tables can be created from scratch. A table's foreign keys are written into its own create statement, so a key pointing at a table the migration has not created yet references nothing. Two tables referencing each other could never be created — neither could go first, the apply threw on the first ALTER, and every later delta was abandoned including the create of the table the key was waiting for, so re-running reproduced the same failure and never recovered. A migration now holds back exactly the keys that would fail, those whose referenced table is created by a later delta, and applies them once every delta has run. Keys whose target already exists, or is created earlier in the same migration, stay where they were, so a schema that never had the problem generates byte-for-byte identical DDL. Only SQL Server and PostgreSQL defer; SQLite writes its foreign keys inline and never had the problem. Note that rolling such a migration back is not symmetrical — on SQL Server neither table of a cycle can be dropped while the other's key points at it, and that is a follow-up.
#543 — a full text index over a tsvector you built yourself. Closes #541; PostgreSQL. FullTextIndexDefinition wrapped whatever it was given in to_tsvector, so DocumentConfig was always text to be converted and there was no way to hand it an expression that was already a tsvector. That put per-member weighting out of reach, because setweight labels a vector — weighting works by converting each member separately and concatenating the vectors, not the text. TsVectorExpression is consumed without the wrapping, via the ForTsVector factory. Leave it unset and the definition behaves exactly as it always has, down to the byte, which matters because a changed index expression makes Weasel drop and recreate the index. Read IndexedTsVector to find what is actually indexed: the DDL comes from that property and nothing else, so a consumer building the query-side filter should read it from there too — a ts_rank computed over a different vector than the one @@ filtered on is silently wrong rather than merely slow. This unblocks the setweight half of JasperFx/marten#5298.
#545 — SQLite delete-all no longer clears the wrong schema's table. GenerateDeleteAllSql built every statement from the table name alone and threw the schema away, so each went out unqualified and SQLite resolved it by search order — which puts temp first. A temp table silently took the delete meant for the main one, and sqlite_sequence is per-database for the same reason.
#546: on SQLite, resetting identity against a schema that has no AUTOINCREMENT table fails with no such table: <schema>.sqlite_sequence, because that schema has no sqlite_sequence. The single-schema form of this predates 9.29; the multi-schema form is new with #545 and is the price of that fix being correct. Calling GenerateDeleteAllSql yourself you can pass resetIdentity: false; going through DatabaseCleaner you cannot.
Full upgrade guide: https://weasel.jasperfx.net/release-9-29
A SQLite release. Five changes, and unlike 9.27 — which was nine cases of Weasel reading a schema back wrong — four of these change the DDL Weasel emi
A SQLite release. Five changes, and unlike 9.27 — which was nine cases of Weasel reading a schema back wrong — four of these change the DDL Weasel emits. A SQLite database created by 9.28 does not have the same column types as one created by 9.27.
If any table declares a column as numeric or decimal, the first migration after upgrading throws on every AutoCreate setting except AutoCreate.All. You get a Weasel.Core.SchemaMigrationException reading Cannot derive schema migrations for … AutoCreate.CreateOrUpdate.
The change itself is safe and the rebuild that applies it copies every row. It is the guard in front of it that refuses: SQLite cannot express a column type change as an ALTER, so the delta comes back as SchemaPatchDifference.Invalid, and AssertPatchingIsValid rejects Invalid on anything but AutoCreate.All regardless of whether a rebuild could apply it. That guard is not specific to this change — it rejects any SQLite column-type change the same way — and fixing it is a follow-up.
Run the first migration with AutoCreate.All, or declare the column real if REAL is what you actually wanted, or stay on 9.27.
#533 — numeric and decimal have NUMERIC affinity, not REAL. SQLite gives a declared type REAL affinity only when it contains REAL, FLOA or DOUB. NUMERIC and DECIMAL get NUMERIC affinity, and the two store data differently: NUMERIC keeps a whole number an integer, REAL converts it to a float. An id declared numeric came back as 1.0 rather than 1. This applies to a type declared as a string; AddColumn<decimal> is unchanged and still maps to REAL.
#532 — a column keeps the type it was declared with. TableColumn's constructor ran every type through ConvertSynonyms, collapsing declared types onto SQLite's storage classes. SQLite stores the declared text verbatim and derives affinity from it by substring rules, so this was not cosmetic: a model asking for TIMESTAMP (NUMERIC affinity) got a TEXT column, and reading an existing database back lost what that database actually said. This is what makes Weasel usable against a SQLite database it did not create.
#534 — NOT NULL survives on a primary key column. The column declaration suppressed NOT NULL whenever it emitted an inline PRIMARY KEY, on the grounds that SQLite applies it implicitly. It does not. Outside a WITHOUT ROWID table only INTEGER PRIMARY KEY is safe, and only because it is a rowid alias whose NULL is replaced by the next rowid rather than stored. A REAL or TEXT primary key stores the NULL, so a rebuilt table accepted keys the original rejected.
#531 — a view body is split on its own AS. This one is not SQLite-only; it affects SQL Server too. Both providers found a view's body by taking everything after the first " AS ", which is wrong whenever the view's own AS stands on a line by itself — the first match is then a column alias inside the SELECT list. It failed silently: both sides of a delta went through the same extraction, so the truncation cancelled out and the comparison reported no drift, while the view that would have been rebuilt was not valid SQL at all. Extraction now lives in Weasel.Core.ViewDefinition.ExtractBody and skips comments, string literals, delimited identifiers and parenthesised column lists. PostgreSQL and Oracle are unaffected — their catalogs return the body already.
#535 — STRICT tables accept the types they are given. A STRICT table accepts only INT, INTEGER, REAL, TEXT, BLOB and ANY, so declaring a column numeric or decimal on one failed outright with unknown datatype. NUMERIC now maps to ANY there — the one STRICT type that keeps a whole number an integer rather than converting it to a float. A parameterized type such as VARCHAR(255) was rejected the same way, and for longer than the other changes have existed; it now maps to TEXT.
Beyond the numeric/decimal break above, two things are worth knowing.
Existing tables are not rebuilt for #532: comparison normalizes both sides, so a column stored as TEXT and a model saying DATETIME still compare equal. But a table rebuilt for some other reason is recreated from the model's declared types, and a value that parses as a number — a bare 2024 in a DATE column, say — is copied into the new column as a number rather than as text.
And if you match existing views by declaring them in Weasel, a view whose stored text put AS on its own line was being compared on a truncated body. It will now compare on the whole one, which may surface a delta that was previously invisible.
Full notes: https://weasel.jasperfx.net/release-9-28
One fix since 9.27.1, and like that release it is a case of the guard existing but not everywhere it needed to.
One fix since 9.27.1, and like that release it is a case of the guard existing but not everywhere it needed to.
#525 — a static partition alongside a partition manager is now refused in every order. A partition manager owns the whole partition set, so a statically declared partition sitting next to one is silently ignored: the caller wrote something that cannot do what they meant.
RangePartitioning.AddRange already refused this. ListPartitioning.AddPartition did not, and neither class guarded UsePartitionManager — so even where the refusal existed, it depended on the order the fluent calls happened to be written in:
| Order | Range | List |
|---|---|---|
| manager, then static partition | threw | accepted |
| static partition, then manager | accepted | accepted |
All four now throw. A fluent builder gives the caller no reason to prefer one order, so the guard should not depend on one either.
Two smaller things fall out of the same change: ListPartitioning.UsePartitionManager now null-checks its argument like its range twin (a null manager was accepted and then silently fell back to the static partitions), and UsePartitionManager clearing EnableDefaultPartition — load-bearing rather than incidental, since it is what made the empty enumeration in #520 total rather than merely short — now has a test pinning it.
AddPartitionWithSqlLiterals is deliberately left unguarded and pinned as such: introspection populates a read table's partitions, and a table read back out of the catalog never carries a manager.
Upgrading. If a configuration of yours declared a static partition alongside a manager, it was already being ignored — you now get an exception at configuration time instead of silence. Remove whichever of the two you did not mean.
Full notes: https://weasel.jasperfx.net/release-9-27
One fix since 9.27.0, and it is the same shape as the nine that release carried: a path that failed without saying so.
One fix since 9.27.0, and it is the same shape as the nine that release carried: a path that failed without saying so.
#526 — a parameterless statement in a batch could be silently discarded. BatchBuilder.AppendWithParameters, in both the PostgreSQL and SQL Server dialects, wrote its SQL into the shared builder but created the underlying batch command only as a side effect of appending a parameter. A statement with no placeholders never reached that code, so the next StartNewCommand() cleared the builder and the SQL was gone — no exception, no log.
Whether it bit you depended on the caller's loop, not on the SQL. Callers that call StartNewCommand() ahead of every operation (Marten) were never affected. Callers that only separate operations were: polecat#517, where a statement queued through IDocumentSession.QueueSqlCommand disappeared whenever the same session also carried a document operation, while SaveChangesAsync reported success.
AppendWithParameters now pins the command down before writing anything — the same guard Append, AppendParameter and AddParameters have always carried. Nothing to do on upgrade.
Full notes: https://weasel.jasperfx.net/release-9-27
Nine fixes since 9.26.0. Every one is a case where Weasel read a schema back as something other than what the database actually held. No DDL changes f
Nine fixes since 9.26.0. Every one is a case where Weasel read a schema back as something other than what the database actually held. No DDL changes for a schema it was already reading correctly, and no configuration changes.
Full upgrade guide: Upgrading to 9.27
A delta that cannot converge is worse than a wrong one — the patch is applied, the read-back is still different, and the next run produces the identical patch indefinitely.
pragma_foreign_key_list has no name column, so the read invented one. Every foreign key Weasel had itself written came back under a name the model did not have — and on SQLite that is repaired by rebuilding the table and copying every row.sys.index_columns row for the partitioning column was read as a declared key column, so the index compared unequal to itself. The rebuilt index read back the same way.ListPartitioning.PartitionTableNames ignored the partition manager and returned the empty sequence, so only the metadata-only first step of the three-step sequence rendered. Strictly worse than the blocking CREATE INDEX it replaced.42601: syntax error at or near "select". Any migration containing one of them plus another object failed, unless the unterminated one happened to be last. Fixed on PostgreSQL, SQLite and MySQL; SQL Server's were unaffected in practice but are terminated now too. Oracle stays deliberately unterminated, and a conformance test asserts all three facts.(a, b) and (b, a) are different indexes with different query plans.(a DESC, b) read back as (a, b DESC), and a genuinely different index was never reported as drift.pg_inherits returned.IgnoreIndex was ignored entirely (#511) — the generated patch dropped the very index it was meant to protect.SetPrimaryKeyOrder / HasExplicitPrimaryKeyOrder on TableBase, and DescendingColumns / CompareColumnDirection on SQL Server's IndexDefinition.
Both are opt-in, deliberately: reading order faithfully must not become comparing it for a model that never expressed one. A model that only flags columns cannot say (c, a), so comparing unconditionally would report drift the user cannot resolve — and "fixing" it is a table rebuild on a clustered key, or a full row copy on SQLite.
Built from a fresh clone against fresh containers for all five databases: 18 of 18 suites green, 0 failures, across net9.0 and net10.0 — PostgreSQL under both case-sensitivity settings.
Ten changes since 9.25.2. A minor rather than a patch — see Upgrade notes below.
Ten changes since 9.25.2. A minor rather than a patch — see Upgrade notes below.
Full notes: Upgrading to 9.26
Two behaviours change in ways you can observe, and one adds public API. Emitted DDL is otherwise unchanged.
DbObjectName.Schema and .Name now hold the undelimited name (#500). If you compare either against a literal carrying quotes or brackets, that comparison changes meaning. Marten and Wolverine were checked and carry none. Bare names are byte-identical.IDatabaseProvider gains a Rules property (#509) — virtual, null by default, so a provider implemented outside Weasel keeps compiling.These four are worth reading even if nothing looks broken from where you are sitting.
Create forever, CREATE TABLE IF NOT EXISTS did nothing, and every later change was discarded while ApplyAllAsync returned normally. On SQL Server it surfaced loudly instead, as There is already an object named 'x' in the database.pg… (#504). pgcontrol, pgqueues, pgdata — the primary key still arrived, so the table looked correct and only its non-PK indexes were missing. The resulting bare create index then failed with 42P07 on the second startup and aborted the rest of the migration.db-assert agreed the schema was correct. PostgreSQL leaves one behind whenever CREATE INDEX CONCURRENTLY fails partway.CREATE on the database before evaluating CREATE SCHEMA's own IF NOT EXISTS, so a role granted USAGE, CREATE on one schema was refused with 42501 on a schema that already existed — aborting the whole script."my.schema".things, [my.table], or the PK_dbo.__MigrationHistory that EF6 gives its own history table.CREATE INDEX CONCURRENTLY on a partitioned parent, so adding an index meant an ACCESS EXCLUSIVE lock for the whole build — a write outage rather than a migration. Weasel now emits the supported ON ONLY → per-partition concurrent build → ATTACH PARTITION sequence. Nothing changes unless an index is both concurrent and on a partitioned table.#504, #505 and the Wolverine half of #495 came in from JasperFx/wolverine#3997.
Two changes that had been sitting on master since 9.25.1. Full notes: Upgrading to 9.25
Two changes that had been sitting on master since 9.25.1. Full notes: Upgrading to 9.25
#488.
TableBase.CheckConstraints is on the shared base, so every provider's Table accepted a check constraint. Only PostgreSQL and SQL Server ever wrote one into the DDL. On Oracle, MySQL and SQLite the constraint sat in the model, never reached the database, and was never compared during delta detection either — so you got a table without the constraint you asked for and nothing said so.
Both entry points now throw on those three:
table.AddCheckConstraint("ck_orders_qty", "quantity > 0"); // NotSupportedException
table.CheckConstraints.Add(new TableCheckConstraint("ck", "quantity > 0")); // NotSupportedExceptionThe collection refuses as well as the method — adding to it directly is the more common spelling, and throwing from only one would have left the silent path open.
If you call either on Oracle, MySQL or SQLite you now get an exception where you previously got silence. Nothing of value is lost: the constraint was never created either way. But the failure moves from invisible to loud, which is the point of the change, and it is a runtime exception that did not exist before.
All three engines do support check constraints — Weasel does not emit them there yet. The message says so and points at the issue, rather than implying the engine cannot do it.
This is the rule #449 settled, applied to the one place it had been missed. It was missed because the property lives on TableBase rather than on each provider's own type, so there was no per-provider surface to audit.
#492.
Six Oracle object types — views, triggers, stored procedures, packages, synonyms and functions — had each written out the same delta class after hitting the same failure separately. They now share OracleReplaceDelta. 209 lines removed, 40 added.
No behaviour change, and nothing to do on upgrade. Five of the six replaced classes were internal; the public Weasel.Oracle.Functions.FunctionDelta stays and delegates to the shared rule.
The rule itself is worth stating correctly, because the shorthand was wrong. It is not "one statement per delta" — a package legitimately emits its specification, a /, and then its body, and the migrator runs two commands. It is do not prefix anything to the create: Oracle has no DROP … IF EXISTS before 23c, so every drop is an anonymous PL/SQL block, and one written immediately before a CREATE OR REPLACE reaches ODP.NET as a block followed by DDL in a single command — PLS-00103.
Everything in 9.25.0 and 9.25.1 still applies. If you are coming from 9.24, read those first — 9.25.1 in particular, which fixes a regression that refuses to create tables 9.24 accepted.
Skip 9.25.0 and take this one. It fixes a regression that refuses to create tables 9.24 accepted.
Skip 9.25.0 and take this one. It fixes a regression that refuses to create tables 9.24 accepted.
Full notes: Upgrading to 9.25
#485 and #486 — one root cause, two symptoms.
9.25.0 began validating the names a table writes that are not database objects of their own — its columns, its primary key constraint, its check constraints (#468). It ran them through the provider's AssertValidIdentifier, which also enforces the length limit. Schemas that applied cleanly on 9.24 started throwing.
The names it rejected are entirely conventional. They are just long — which a wide composite primary key on a long table name produces easily:
pkey_mt_doc_bug_4679_catch_up_per_tenant_23505_tripdistance_tenant_id_id (72 chars)
Weasel's own code already said so. A object name the database truncates becomes unaddressable and drifts on every check afterwards — worth refusing. A local identifier is only emitted inside its own table's DDL and never addressed by name again, and TableDelta already compares both PrimaryKeyName and the primary key column list through TruncatedNameIdentifier precisely so a truncated one still matches.
Weasel was handling truncated local identifiers downstream while refusing to create them upstream. Those two positions could not both be right.
Migrator.AssertValidLocalIdentifier now applies the same safety rules with no length limit — a quote, a semicolon, a line break, leading or trailing whitespace are still rejected wherever they appear. The base implementation defers to AssertValidIdentifier, so a provider outside this repository keeps the stricter behaviour until it opts in.
#486 presented as a 23505 on pk_mt_event_progression during a Marten per-tenant daemon catch-up — an error naming a table with nothing to do with identifiers.
The rejection had aborted a projection's schema application. The daemon then ran against storage that was never created and failed downstream. The loud failure and the quiet one were the same rejection. If you saw anything strange on 9.25.0, rule this out before looking further.
Functions on MySQL and Oracle (#482) — the half of #450 that closed on its views and left this behind. Both catalogs store what their stored-procedure counterparts store, so comparison works the same way.
That completes the object type matrix apart from check constraints on Oracle, MySQL and SQLite (#488) — accepted by TableBase on all five providers and emitted by only two. Pre-existing rather than a 9.25 change, and tracked.
Everything in 9.25.0 still applies — the identifier quoting changes, the column-name rewrite removal, the SQLite data-loss fix, the Oracle migration-path fix, and the one-time schema fingerprint re-evaluation. Read those notes too.
The provider parity release. Twelve issues, fifteen PRs, and every object type in the support matrix covered except functions on MySQL and Oracle ( #4
The provider parity release. Twelve issues, fifteen PRs, and every object type in the support matrix covered except functions on MySQL and Oracle (#482).
Full upgrade notes: Upgrading to 9.25
Neither was reported by a user. Both were found by the parity work, and both had been failing quietly.
TableDelta has always had a careful rebuild: create a new table, copy the surviving columns, drop the old one, rename. The migrator never called it. A change needing a rebuild reports Invalid, and Invalid was answered by dropping the object and creating it again.
Measured on a column type change: one row before, zero after — with a schema that looked entirely correct afterwards.
Affects every SQLite change ALTER TABLE cannot express — a column type change, a foreign key added or removed, a primary key change — under AutoCreate.All, which is what a developer resetting a local database and db-apply on an environment configured for it both use.
If you applied such a change on 9.24 or earlier, that data is gone and this release cannot bring it back. It will not happen again.
ODP.NET will not execute several statements from one command, so an Oracle schema object could register exactly one introspection query — and Table spent it on columns. Everything else was invisible to the entire migration path.
A declared index was created with the table and never touched again. Adding one to an existing table did nothing. Changing one did nothing. Removing one left it in place. And AssertDatabaseMatchesConfigurationAsync reported a clean match throughout.
Expect your first Oracle run on 9.25 to apply index and foreign key changes it had been ignoring. That is the backlog being worked off, not new drift.
Column names are no longer rewritten (#458). PostgreSQL, Oracle, SQLite and SQL Server turned a space into an underscore — but only in TableColumn, so an index over Order Date named a column that did not exist. A column declared "Order Date" is now created as "Order Date". On an existing database that reads as a new column; write the underscore yourself if you were relying on the rewrite.
Identifiers are quoted where they previously were not on SQL Server and MySQL. Ordinary schemas are byte-identical. Anything hashing DDL — the schema fingerprint stamps — re-evaluates once on the first run after upgrade. That is a one-time "everything looks changed" pass, not drift.
Weasel.MySql.Sequence is [Obsolete]. MySQL has never had CREATE SEQUENCE at any version; that is MariaDB 10.3. The emulation could not be consumed. Use AUTO_INCREMENT.
| Views on SQL Server, Oracle and MySQL | All five providers now model views (#450) |
| Triggers, on all five providers | The one whole category no provider modelled (#452) |
| Stored procedures on PostgreSQL, MySQL and Oracle | Plus a shared StoredProcedureBase (#451) |
| Oracle packages, materialized views and synonyms; SQL Server synonyms; PostgreSQL enums, domains and composites | (#453) |
| A shared identifier quoting contract | IdentifierRules in Weasel.Core, with a cross-provider conformance suite (#447) |
| Every identifier a table writes is validated | Column names, primary key and check constraint names were previously validated by no provider (#448) |
| A shared index scenario matrix | Eleven create-then-introspect scenarios per provider (#449) |
| Oracle teardown drops what it used to leave behind | Triggers, packages, synonyms, object types, materialized views (#465) |
Three new documentation pages: Object Type Support, Identifiers and Quoting, Triggers and Stored Procedures.
Four properties that used to accept a value and silently do nothing now throw, naming the supported alternative: IndexDefinition.Predicate on MySQL and Oracle, Sequence.IncrementBy on MySQL, Trigger.Condition on SQL Server and MySQL, and Trigger.Timing = Before on SQL Server.
Parallel db-apply / db-assert ( #431 , #442 )
db-apply / db-assert (#431, #442)The db-apply and db-assert commands can now run migrations across many physical databases in parallel via a new --parallel N flag. Work is grouped by physical database, so all inputs targeting the same database stay sequential while distinct databases proceed concurrently.
Two deliberate behavior changes ship with this:
db-assert reports unexpected exceptions as failures. An exception thrown while asserting a database (connection refused, bad credentials, etc.) is now counted and reported as that database's failure instead of tearing down the whole run.pragma_table_xinfo (#426, #433)CREATE DATABASE by the postcondition (#415, #441)fix(sqlite,mysql): implement the non-generic Weasel.Core.ICommandBuilder (weasel#423) by @jeremydmiller in #424
Weasel.Core.ICommandBuilder (weasel#423) by @jeremydmiller in #424JasperFx 2.38.0 brings the lifted ProjectionScenario test harness (JasperFx/jasperfx#616) and the container-scoped projection fixes for live aggregation and validation (JasperFx/jasperfx#617). Nothing Weasel consumes changed; the bump keeps the dependency line current so downstream stores can take one coherent update.
Full Changelog: V9.23.1...V9.23.2
Bring the other four providers' AssertValidIdentifier in line with PostgreSQL (weasel#416) by @jeremydmiller in #420
Full Changelog: V9.23.0...V9.23.1
Two concurrency/correctness fixes, plus the JasperFx 2.37.0 floor that was staged in 9.22.0 but never shipped — so 9.23.0 carries all three.
Two concurrency/correctness fixes, plus the JasperFx 2.37.0 floor that was staged in 9.22.0 but never shipped — so 9.23.0 carries all three.
SqlServerMigrator.EnsureDatabaseExistsAsync is safe under concurrent callers (#415)The check-then-create against master left the loser of the race holding SqlException 1801 — "Database 'x' already exists", and the method returned before the newly created database would accept logins. Neither mattered while a single process bootstrapped a single database at startup; both matter as soon as parallel test workers each provision their own database against a cold container.
IF DB_ID(...) IS NULL CREATE DATABASE narrows the window but SQL Server does not make that pair atomic, so the catch is what closes it.TimeoutException naming the database, and is configurable via DatabaseAvailabilityTimeout (default 30s) and DatabaseAvailabilityPollingInterval (default 1s). TimeSpan.Zero fails fast.] in a database name is escaped.PostgresqlMigrator.EnsureDatabaseExistsAsync got the matching 42P04 duplicate_database catch. It needs no availability wait — Postgres accepts connections to a new database as soon as CREATE DATABASE returns.This promotes logic that already existed in SqlServerDatabaseBootstrap in the EF Core test project into the shipped assembly; that helper now delegates.
AssertValidIdentifier rejects " and ; (#416)Closes out the identifier half of #416, whose literal-escaping half shipped in 9.21.1. PostgresqlMigrator.AssertValidIdentifier is the only identifier check in the stack — DbObjectName and PostgresqlObjectName do no validation — yet it permitted the two characters that let an object name escape the statement it is written into: " closes a quoted identifier, and ; starts a new statement. It also now rejects all whitespace rather than just the literal space, so a newline cannot introduce a -- comment.
PostgresqlIdentifierInvalidException gained a constructor that names the rule that was broken; the existing one is untouched. DbObjectName and PostgresqlObjectName are now documented as not being sanitizing boundaries.
Full changelog: V9.21.0...V9.23.0
Nothing published for this version
Migrate all test projects to xUnit v3 by @jeremydmiller in #393
Full Changelog: V9.20.0...V9.21.0
Nothing published for this version
Nothing published for this version
Make managed tenant partition bucketing actually share a partition ( #391 ) by @jeremydmiller in #392
Full Changelog: V9.19.1...V9.20.0
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →