Skip to content

Import Marten.PostGIS and Marten.PgVector from CritterWatch#4576

Merged
jeremydmiller merged 3 commits into
masterfrom
feature/import-postgis-pgvector
May 28, 2026
Merged

Import Marten.PostGIS and Marten.PgVector from CritterWatch#4576
jeremydmiller merged 3 commits into
masterfrom
feature/import-postgis-pgvector

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Imports the four PostGIS / pgvector projects originally prototyped under ~/code/critterwatch/tools/ into Marten itself, to be shipped under Marten's MIT license. No removal from CritterWatch is included in this PR (per instruction — that'll be a follow-up after the NuGets ship).

What lands

Two new library projects

Project Purpose
src/Marten.PostGIS UsePostGIS() opt-in that enables the postgis extension on every database Marten manages (multi-tenant aware), wires NetTopologySuite + GeoJSON serialization, and exposes four query helpers — NearestToAsync, WithinDistanceAsync, ContainingAsync, IntersectingAsync.
src/Marten.PgVector UsePgVector() opt-in that enables the vector extension on every database (also addresses #2515). VectorSearchAsync for similarity queries, plus an embedding-aware VectorProjection base class with content-hash skipping. AI-model-agnostic (IEmbeddingProvider).

Both follow Marten's Marten.NodaTime csproj template — multi-target net9.0;net10.0, CPM, GenerateAssembly* attrs, RootNamespace, etc.

Test projects

Tests use Marten.Testing.Harness.ConnectionSource (port 5432, marten_testing DB) rather than CritterWatch's local 5442/postgres. The database-per-tenant test creates pgvector_tenant{1,2,3} on demand following the same pattern as DocumentStore_IMartenStorage_implementation.

Wiring

  • Added to src/Marten.slnx under new PgVector / PostGIS solution folders.
  • Directory.Packages.props pins: NetTopologySuite 2.5.0, NetTopologySuite.IO.GeoJSON 4.0.0, Npgsql.NetTopologySuite 9.0.4, Pgvector 0.3.2.
  • build/build.cs adds TestPostGIS + TestPgVector targets, both hung off TestExtensions. Nuke's .nuke/build.schema.json regenerates to match.

Docker

Replaces the legacy ionx/postgres-plv8:12.8 (Postgres 12 — PLv8 only) image with a tiny custom docker/postgres/Dockerfile that layers postgresql-17-postgis-3 + postgresql-17-pgvector on the official multi-arch postgres:17 image. Works on Apple-silicon hosts. Marten core SQL no longer requires PLv8 — only vestigial InternalsVisibleTo("Marten.PLv8") declarations remain in AssemblyInfo.cs — so the drop is safe.

CI

New workflow .github/workflows/on-push-do-ci-build-postgis-pgvector.yml runs the two extension test suites side-by-side, each against the upstream image that ships the corresponding extension:

  • postgis/postgis:17-3.5 for the PostGIS suite
  • pgvector/pgvector:pg17 for the PgVector suite

Existing CI workflows (PG15/PGLatest matrix) are untouched.

Docs

There were no existing PostGIS/PgVector docs in CritterWatch's docs/, so this PR includes fresh VitePress pages authored from the imported code & tests:

  • docs/postgres/postgis.md
  • docs/postgres/pgvector.md

Both wired into the sidebar in docs/.vitepress/config.mts. Both pass markdownlint --disable MD009 and cspell --config ./docs/cSpell.json locally (matching the docs-prs.yml CI invocations). pgvector and Ollama added to docs/cSpell.json.

Notes for the reviewer

  • Tests were not executed locally. The existing dev Postgres container in this environment is the old PLv8 image; tearing it down to rebuild from the new Dockerfile would have disrupted other in-flight work. The full solution compiles cleanly (0 errors). The new CI workflow exercises the suites against extension-enabled images.
  • TenancyStyle reference in conjoined_vector_tests.cs updated from Marten.Storage.TenancyStyle to the JasperFx-lifted name, picked up via the existing global alias in src/Shared/DedupeAliases.cs.
  • Marten.PostGIS references Marten.Newtonsoft because JsonNetSerializer moved there in Marten 9. The PostGIS UsePostGIS() registers the NTS GeoJSON converters on the Newtonsoft serializer.
  • Version bump not includedDirectory.Build.props still says 9.2.1, matching master. Bump and release as a separate step once these compile and pass on CI.

🤖 Generated with Claude Code

jeremydmiller and others added 3 commits May 28, 2026 08:28
Adds two optional companion packages — `Marten.PostGIS` and
`Marten.PgVector` — under Marten's MIT license, importing the projects
originally prototyped under `~/code/critterwatch/tools/`. CritterWatch
itself is left untouched (per instruction, no removal yet).

What lands here:

- **`src/Marten.PostGIS`** + tests (3 source files + 1 test class).
  `UsePostGIS()` opt-in that registers the `postgis` extension on every
  database Marten manages (multi-tenant aware), wires
  NetTopologySuite + GeoJSON serialization, and exposes four query
  helpers — `NearestToAsync`, `WithinDistanceAsync`, `ContainingAsync`,
  `IntersectingAsync`.
- **`src/Marten.PgVector`** + tests (7 source files + 4 test classes
  covering single-tenant, conjoined multi-tenancy, database-per-tenant,
  and the embedding-aware `VectorProjection`). `UsePgVector()` opt-in
  that registers the `vector` extension on every database — also
  addresses the long-standing #2515 (extensions not created in tenant
  databases). `VectorSearchAsync` + a `VectorProjection` base class
  with content-hash skipping so unchanged content is not re-embedded.

Adapted to Marten conventions:

- Both libraries multi-target via `Directory.Build.props`
  (net9.0;net10.0), use CPM, and follow the existing `Marten.NodaTime`
  csproj template (Description + GenerateAssembly* attrs).
- Tests use `Marten.Testing.Harness.ConnectionSource` (port 5432,
  `marten_testing` DB) instead of the CritterWatch-local `5442`/`postgres`.
  The database-per-tenant test creates `pgvector_tenant{1,2,3}` on
  demand mirroring `DocumentStore_IMartenStorage_implementation`.
- `Marten.PostGIS` references `Marten.Newtonsoft` — `JsonNetSerializer`
  moved there in Marten 9.
- `TenancyStyle` reference updated to the JasperFx-lifted name (via
  the global alias in `src/Shared/DedupeAliases.cs`).

Wiring:

- All four projects added to `src/Marten.slnx` under new `PgVector` /
  `PostGIS` folders.
- `Directory.Packages.props` pins `NetTopologySuite 2.5.0`,
  `NetTopologySuite.IO.GeoJSON 4.0.0`, `Npgsql.NetTopologySuite 9.0.4`,
  `Pgvector 0.3.2`.
- `build/build.cs` gets `TestPostGIS` + `TestPgVector` targets, both
  hung off `TestExtensions` (the Nuke schema regenerates to match).

Docker:

- Replaces `ionx/postgres-plv8:12.8` (Postgres 12 — only PLv8) with a
  tiny custom Dockerfile at `docker/postgres/Dockerfile` that layers
  `postgresql-17-postgis-3` + `postgresql-17-pgvector` on the official
  multi-arch `postgres:17` image. Multi-arch means it works on
  Apple-silicon hosts. Marten core SQL no longer uses PLv8 (only
  vestigial `InternalsVisibleTo` declarations remain) so the drop is
  safe.

CI:

- New workflow `.github/workflows/on-push-do-ci-build-postgis-pgvector.yml`
  runs the two extension test suites side-by-side, each against the
  upstream `postgis/postgis:17-3.5` and `pgvector/pgvector:pg17` images
  respectively. Existing CI workflows (PG15/PGLatest matrix) are
  untouched.

Docs:

- Fresh `docs/postgres/postgis.md` and `docs/postgres/pgvector.md`
  (there were no existing docs in CritterWatch). Wired into the
  sidebar in `docs/.vitepress/config.mts`; both pass markdownlint +
  cspell. `pgvector` and `Ollama` added to `docs/cSpell.json`.

Tests were not run locally — the existing dev Postgres container is
the old PLv8 image and rebuilding it could disrupt other in-flight
work in this environment. CI exercises the suites against the
extension-enabled images above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CI failure on PR #4576: one of fourteen PgVector tests
(`conjoined_vector_tests.vector_search_respects_tenant_isolation`)
failed with `Npgsql.PostgresException 23505: duplicate key value
violates unique constraint "pg_extension_name_index"
(extname)=(vector)`.

Root cause: each PgVector test class spins up its own DocumentStore
that calls `UsePgVector()`, registering a `CREATE EXTENSION IF NOT
EXISTS vector` schema object. PostgreSQL's `CREATE EXTENSION IF NOT
EXISTS` is *not* race-safe — concurrent callers can both pass the
pre-existence check before either has inserted into `pg_extension`,
and the loser hits `23505` on `pg_extension_name_index`. xUnit's
default puts each test class in its own auto-collection and runs
collections in parallel, so the four classes raced against themselves
on the shared `marten_testing` DB.

Fix: add a single `[CollectionDefinition("Marten.PgVector",
DisableParallelization = true)]` and apply `[Collection("Marten.PgVector")]`
to all four classes — sequential within the suite, still parallel
with other (non-PgVector) collections in other assemblies. Also
replaces the prior `[Collection("multi-tenancy")]` on the
database-per-tenant test, which was a different collection from the
others and therefore did not prevent the race.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second CI failure on PR #4576 — `vector_search_respects_tenant_isolation`
threw:

    System.InvalidCastException : Writing values of 'Pgvector.Vector'
    is not supported for parameters having no NpgsqlDbType or DataTypeName.
    ---- System.NotSupportedException : Cannot resolve 'vector' to a
    fully qualified datatype name. The datatype was not found in the
    current database info.

Cause: `b.UseVector()` registers a `Pgvector.Vector → "vector"` mapping
on the NpgsqlDataSource, but the data source caches pg_type the first
time it opens a connection. When Marten's schema migration creates the
`vector` extension *on the same data source* (the typical `UsePgVector()`
flow), the cache stays stale and subsequent `Pgvector.Vector` parameter
binds throw on OID lookup. Fresh data sources are race-fragile across
test classes because of the cache-on-first-connection rule.

Fix: bind the vector value as its text form (`[f1,f2,…]`, returned by
`Pgvector.Vector.ToString()`) and cast to `vector(N)` server-side
(`$1::vector(N)`). Text is a builtin Npgsql type — no extension-type
lookup, no cache dependency, race-immune. Applied in all three call
sites: `VectorSearchAsync`, `VectorProjectionSearchAsync`, and
`VectorProjection`'s embedding upsert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jeremydmiller jeremydmiller merged commit 23fb451 into master May 28, 2026
8 of 9 checks passed
@jeremydmiller jeremydmiller deleted the feature/import-postgis-pgvector branch May 28, 2026 16:00
jeremydmiller added a commit that referenced this pull request May 28, 2026
…ths)

CI failure on PR #4578: every test that exercises an event append on
Quick / QuickWithServerTimestamps modes — including the Daemon tests
and the PgVector projection tests on the prior PR #4576 import —
failed with:

    Npgsql.PostgresException : 42601: INSERT has more target columns
    than expressions

Root cause: the previous commit added `bdata` to `EventsTable.SelectColumns`
(SELECT position 3) and consequently to the SQL prefix built by
`PostgresEventStoreDialect.BuildAppendEventFullColumnsAndPrefix` (used by
*every* full-shape INSERT — Rich AND the per-event Quick path). I only
wired the bind in `RichAppendEventOperation`. The siblings —
`QuickAppendEventWithVersionOperation` (used by RichEventStorage,
QuickEventStorage, and QuickWithServerTimestampsEventStorage for
per-event INSERTs: tombstone batches, new-stream appends, optimistic-
concurrency appends, side-effect replay through EventSlice.BuildOperations)
— still emitted the old N parameters against the now-N+1 column list.

Fix: thread a `Func<IEvent, byte[]?> SerializeEventBdata` closure
through both Quick descriptors and `QuickAppendEventWithVersionOperation`,
mirroring the Rich descriptor's existing field. The closure binds
`bdata` immediately after `mt_dotnet_type` to match the column-list
position. In Quick modes the dialect installs `_ => null` since binary
events are rejected at descriptor-build time anyway.

This is what was missing from PR #4578's initial commit — the Rich-mode-only
constraint covers the *binary opt-in*, but `bdata` is still a column on
every row (NULL for JSON events), so every full-shape INSERT has to bind
it regardless of mode.

Local repro now passes:
- DaemonTests.Bug_3059_double_application: 1/1 ✅ (was the canary failure)
- Marten.MemoryPack.Tests: 5/5 ✅ (unaffected — binary path was already correct)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller added a commit that referenced this pull request May 28, 2026
… only) (#4578)

* Foundation for binary event serialization (#4515 — Phase 1, Rich-mode only)

Adds per-event-type binary serialization on the event store side, with
JSON-serialized and binary-serialized events coexisting in the same
mt_events table. Designed so the feature can be turned on in an
existing store with no migration of existing event data.

## Coexistence design

Schema is purely additive — one new column on mt_events:

  data  jsonb NOT NULL   -- existing; for binary events, holds {} placeholder
  bdata bytea NULL       -- new; the serialized bytes for binary events,
                          --      NULL for JSON-serialized events

Per-row discriminator: bdata IS NULL ⇒ JSON path, non-null ⇒ binary.
The existing `data NOT NULL` constraint stays intact, so the migration
is one nullable column — safe for in-place upgrade. Existing rows in
an upgraded store have `bdata = NULL` and continue to read through the
JSON path unchanged.

## Public API

  public interface IEventBinarySerializer
  {
      byte[] Serialize(Type type, object data);
      object Deserialize(Type type, byte[] data);
  }

Two equivalent registration paths:

  // 1. Attribute-driven (uses opts.Events.DefaultBinarySerializer as resolver)
  [BinaryEvent]
  [MemoryPackable]
  public partial record TripStarted(...);

  // 2. Fluent (explicit per-type serializer)
  opts.Events.UseBinarySerializer<TripStarted>(new MemoryPackEventSerializer());

Resolution order on EventMapping construction:
  1. Explicit UseBinarySerializer<T>(...) for that type
  2. [BinaryEvent] + opts.Events.DefaultBinarySerializer
  3. Otherwise, plain JSON (existing path).

If `[BinaryEvent]` is set but neither a per-type nor a default
serializer is wired, EventMapping throws at construction with a
remediation message naming both entry points.

## Surface area touched

- src/Marten/Events/{IEventBinarySerializer,BinaryEventAttribute}.cs       (new)
- src/Marten/Events/Schema/EventBdataColumn.cs                              (new — bytea nullable column)
- src/Marten/Events/Schema/EventsTable.cs                                   (add column; pin at SELECT position 3)
- src/Marten/Events/IEventStoreOptions.cs                                   (DefaultBinarySerializer + UseBinarySerializer<T>)
- src/Marten/Events/EventGraph.cs                                          (registry + ResolveBinarySerializerFor)
- src/Marten/Events/EventMapping.cs                                        (BinarySerializer + IsBinary)
- src/Marten/Events/EventDocumentStorage.cs                                (per-row JSON-vs-binary dispatch in Resolve/ResolveAsync)
- src/Marten/EventStorage/Rich/RichAppendEventOperation.cs                 (bind bdata at column slot 4)
- src/Marten/EventStorage/Rich/RichEventStorageDescriptor.cs               (SerializeEventBdata closure)
- src/Marten/EventStorage/Dialects/PostgresEventStoreDialect.cs            (bdata in IsCoreColumn; SerializeEventBdata wiring; Quick-mode guard)
- src/Marten/EventStorage/ClosedShapeEventDocumentStorage.cs               (Skip(3)→Skip(4); +4 ordinal offset)

## Marten.MemoryPack package

  - src/Marten.MemoryPack/                — IEventBinarySerializer impl over MemoryPack 1.21.4
  - src/Marten.MemoryPack.Tests/          — 5 integration tests (round-trip,
                                            multi-event replay, mixed JSON+binary
                                            stream, on-disk shape verification,
                                            upgrade-backfill against a pre-existing
                                            JSON row)

Added to Marten.slnx under a `/MemoryPack/` folder; TestMemoryPack
target wired into build.cs and hung off TestExtensions; MemoryPack
1.21.4 pinned in Directory.Packages.props.

## Phase 1 scope — explicit limitations (documented in docs/events/binary-serialization.md)

- **EventAppendMode.Rich only.** The default `QuickWithServerTimestamps`
  and `Quick` modes go through the `mt_quick_append_events` PostgreSQL
  function whose signature would need a parallel `bdata bytea[]`
  parameter. BuildQuickDescriptor / BuildQuickWithServerTimestampsDescriptor
  fail loud at store-build time if a binary event type is registered
  with a non-Rich AppendMode, with the remediation recipe in the
  exception message. Quick-mode support is the Phase 2 scope.
- **No BulkEventAppender support.** Same root cause — the COPY column
  shape needs the bdata column adding. Follow-up.
- **No binary upcaster support.** Marten's JSON upcasters
  (Marten.Services.Json.Transformations) operate on JSON payloads and
  don't generalize to byte[]. Binary upcasters need their own typed
  transform shape; tracked as a separate follow-up issue.

## Docs

  - docs/events/binary-serialization.md      (new page — coexistence design, registration,
                                              on-disk shape, migration story, constraints)
  - docs/events/optimizing.md                (link from the scalability/optimization page)
  - docs/.vitepress/config.mts               (sidebar entry under Events)

Both pass `markdownlint --disable MD009` and `cspell` locally.

## Test results

  - Marten.MemoryPack.Tests: 5/5 passing
  - EventSourcingTests.end_to_end_event_capture_and_fetching: 83/83 passing
    (regression check — JSON-only events through the dispatched read path)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Bind bdata in QuickAppendEventWithVersionOperation (all Quick/Rich paths)

CI failure on PR #4578: every test that exercises an event append on
Quick / QuickWithServerTimestamps modes — including the Daemon tests
and the PgVector projection tests on the prior PR #4576 import —
failed with:

    Npgsql.PostgresException : 42601: INSERT has more target columns
    than expressions

Root cause: the previous commit added `bdata` to `EventsTable.SelectColumns`
(SELECT position 3) and consequently to the SQL prefix built by
`PostgresEventStoreDialect.BuildAppendEventFullColumnsAndPrefix` (used by
*every* full-shape INSERT — Rich AND the per-event Quick path). I only
wired the bind in `RichAppendEventOperation`. The siblings —
`QuickAppendEventWithVersionOperation` (used by RichEventStorage,
QuickEventStorage, and QuickWithServerTimestampsEventStorage for
per-event INSERTs: tombstone batches, new-stream appends, optimistic-
concurrency appends, side-effect replay through EventSlice.BuildOperations)
— still emitted the old N parameters against the now-N+1 column list.

Fix: thread a `Func<IEvent, byte[]?> SerializeEventBdata` closure
through both Quick descriptors and `QuickAppendEventWithVersionOperation`,
mirroring the Rich descriptor's existing field. The closure binds
`bdata` immediately after `mt_dotnet_type` to match the column-list
position. In Quick modes the dialect installs `_ => null` since binary
events are rejected at descriptor-build time anyway.

This is what was missing from PR #4578's initial commit — the Rich-mode-only
constraint covers the *binary opt-in*, but `bdata` is still a column on
every row (NULL for JSON events), so every full-shape INSERT has to bind
it regardless of mode.

Local repro now passes:
- DaemonTests.Bug_3059_double_application: 1/1 ✅ (was the canary failure)
- Marten.MemoryPack.Tests: 5/5 ✅ (unaffected — binary path was already correct)

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 28, 2026
…arget (#4582)

The Nuke `Pack` target in `build/build.cs` lists every project that
gets packed and pushed by the `on-manual-do-nuget-publish.yml`
workflow. The three new optional companion packages added in PR #4576
(PostGIS / PgVector) and PR #4578 (MemoryPack — binary event
serialization) were never added to the list, so the next NuGet release
would silently leave them off NuGet.

Repacking locally now produces all 9 .nupkgs (was 6):

    Marten.9.2.1.nupkg
    Marten.AspNetCore.9.2.1.nupkg
    Marten.EntityFrameworkCore.9.2.1.nupkg
    Marten.MemoryPack.9.2.1.nupkg          ← new
    Marten.Newtonsoft.9.2.1.nupkg
    Marten.NodaTime.9.2.1.nupkg
    Marten.PgVector.9.2.1.nupkg            ← new
    Marten.PostGIS.9.2.1.nupkg             ← new
    Marten.SourceGenerator.9.2.1.nupkg

Gating fix before the 9.3.0 release.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller added a commit that referenced this pull request May 28, 2026
… fix

Weasel 9.0.2 (JasperFx/weasel#299) fixes
PostgresqlMigrator.executeWithConcurrencyRetryAsync so it reopens a
Closed/Broken connection before the retry attempt. That eliminates
the recurring "Connection is not open" failure on the conjoined
`EventSourcingTests.end_to_end_event_capture_and_fetching_the_stream.
query_before_saving` test that hit this PR + #4576 + #4578 + #4582.

Bumps Weasel.Postgresql + Weasel.EntityFrameworkCore 9.0.1 → 9.0.2 in
Directory.Packages.props (CPM). Marten.MemoryPack.Tests still 8/8
locally on top of the new Weasel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller added a commit that referenced this pull request May 28, 2026
#4584)

* #4515 Phase 2: binary event serialization on Quick + BulkEventAppender

#4578 shipped the foundation with `Rich` mode only; the Quick paths
(QuickWithServerTimestamps default + Quick + bulk COPY) all guarded
against binary events at store-build time. This lifts that constraint —
binary serialization now works on every EventAppendMode and through the
BulkEventAppender.

## Wire-format change: `mt_quick_append_events` grows a `bdatas bytea[]` param

The PostgreSQL function used by both Quick variants now accepts a
parallel `bdatas bytea[]` parameter right after `bodies jsonb[]`. The
INSERT writes `bdatas[index]` into `mt_events.bdata`. For JSON events
the array slot is NULL; for binary events `bodies[index]` is the `{}`
placeholder and `bdatas[index]` carries the real payload. Same
on-disk row shape as the Rich path: `bdata IS NULL` remains the
discriminator that the existing read path keys off.

Weasel's standard function-diff migration handles the signature
change as DROP + CREATE on existing installations; existing JSON rows
are untouched.

## Call-site dispatch — same shape as Rich

`PostgresEventStoreDialect.BuildQuickDescriptor` and
`BuildQuickWithServerTimestampsDescriptor` install the same
`serializeEventData` / `serializeEventBdata` closures the Rich
descriptor uses (look up `EventMapping`, branch on `IsBinary`).
`QuickAppendEventsOperationBase.writeBasicParameters` now accepts an
optional `Func<IEvent, byte[]?> serializeEventBdata` and binds the
parallel `bdatas bytea[]` array.

## BulkEventAppender — bdata in the COPY column list

`buildEventColumns()` adds `bdata` right after `data`; `writeEventRow`
looks up the EventMapping per event and writes either the binary
payload (for `[BinaryEvent]` types) or NULL (for JSON). The COPY
format already supports NULL values per column, so no schema
relaxation is needed.

## Removed: AssertNoBinaryEventsForQuickMode

The Phase 1 guard in `PostgresEventStoreDialect` that threw at
store-build time if a binary event type was registered with a
non-Rich AppendMode is gone — no longer needed.

## Tests

Three new tests in `QuickModeBinaryEventTests` (separate fixture so
each test can dial in its own AppendMode):

- `quick_with_server_timestamps_round_trips_binary_events` — mixed
  binary + JSON stream on the default mode, round-trip via the PG
  function.
- `quick_mode_round_trips_binary_events` — explicit `Quick` mode.
- `quick_mode_binary_events_land_in_bdata_column` — on-disk shape
  verification: binary rows have `data = '{}'` + `bdata != NULL`;
  JSON rows have `data = real JSON` + `bdata = NULL`.

Regression checks:
- Full Marten.MemoryPack.Tests suite: 8/8 ✅
- EventSourcingTests.end_to_end_event_capture_and_fetching: 83/83 ✅
- DaemonTests.Bug_3059_double_application: 1/1 ✅ (re-running the
  test that flushed out the column-count bug in PR #4578's first CI run)

## Docs

`docs/events/binary-serialization.md` updated:
- Removed the "EventAppendMode.Rich only" + "No bulk appender support"
  constraints from the Constraints section.
- Added a new "Append modes" section explaining the feature works
  across all three modes + BulkEventAppender.
- Quick-start example no longer forces `AppendMode = Rich`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Consume Weasel 9.0.2 — picks up the migration-retry connection-reopen fix

Weasel 9.0.2 (JasperFx/weasel#299) fixes
PostgresqlMigrator.executeWithConcurrencyRetryAsync so it reopens a
Closed/Broken connection before the retry attempt. That eliminates
the recurring "Connection is not open" failure on the conjoined
`EventSourcingTests.end_to_end_event_capture_and_fetching_the_stream.
query_before_saving` test that hit this PR + #4576 + #4578 + #4582.

Bumps Weasel.Postgresql + Weasel.EntityFrameworkCore 9.0.1 → 9.0.2 in
Directory.Packages.props (CPM). Marten.MemoryPack.Tests still 8/8
locally on top of the new Weasel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Document the versioned-event-types pattern for binary schema evolution

Closes #4579 with a docs-only answer rather than building a binary-side
upcaster framework. The JSON upcasters
(Marten.Services.Json.Transformations) operate on the JSON wire form and
don't generalize to a byte[] payload; designing a typed transform shape
for binary events is non-trivial and the use case can be addressed
end-to-end today by leaning on Marten's existing per-event-type
registry.

The recommendation: introduce a new event type for each schema change
(e.g. TripStarted -> TripStartedV2), have the aggregate handle both
versions, and let the coexistence design carry old rows + new rows on
the same stream without migration. The only caveat is that MemoryPack's
in-place backward-compatible field evolution works for additive-only
changes too, but stops at the serializer's tolerance rules (renames,
type changes, splits) — versioned event types work for every shape of
change and stay explicit.

Replaces the "No upcaster support" constraint section with a "Schema
evolution — use versioned event types" section that gives the
recommended pattern with code samples + a sub-section on the
"why-not-in-place" tradeoff + a note on mixing binary + JSON.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant