Skip to content

GH-5673 TombstoneStream (hard stream delete) on JasperFx 2.90.0 - #5674

Merged
jeremydmiller merged 2 commits into
masterfrom
GH-5673-tombstone-stream
Oct 15, 2026
Merged

jeremydmiller merged 2 commits into
masterfrom
GH-5673-tombstone-stream

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #5673

What

JasperFx 2.90.0. Bumps all five JasperFx pins from 2.88.0. No Weasel bump: 9.45.1, the only newer Weasel, patches the SQL Server and Oracle providers only, and restore and build are clean on 9.45.0.

TombstoneStream(Guid) / TombstoneStream(string) on session.Events. It is queued and runs at SaveChangesAsync. The work is done by a new internal DeleteStreamOperation, named that way because "tombstone" already means Marten's sequence-gap bookkeeping stream. It goes through the shared seam: PostgresEventStoreDialect.BuildAuxiliaryOperations now fills the TombstoneStream slot, and its doc comment is updated.

  • One statement, built from data-modifying CTEs. It deletes the stream's rows from each mt_event_tag_* table (keyed by the stream's seq_ids), then mt_events, then mt_streams_identity when strict identity enforcement is on, then mt_streams. Natural key rows cascade from mt_streams.
  • Why one statement. Npgsql won't take ;-separated statements in a positional-parameter command. Starting extra batch commands inside one operation would break OperationPage's alignment of one command per operation. All CTEs read the same snapshot, so the tag delete still finds the events, and the FK checks run at the end of the statement. I verified this with partitioned FKs directly in Postgres before writing it.
  • Tenancy. Under conjoined tenancy every delete is scoped by tenant_id. Deleting through the parent tables reaches the archived partition, and the tenant predicate prunes to the tenant's partition under UseTenantPartitionedEvents.
  • Sequence. The event sequence is never touched, so sequence numbers are never reused.
  • Identity check. The stream identity is asserted through EnsureAsGuidStorage / EnsureAsStringStorage, so calling the wrong overload throws InvalidOperationException.

AOT (#1018). The [RequiresUnreferencedCode] / [RequiresDynamicCode] attributes are carried up to RegisterTagType<TTag>() and RegisterTagType<TTag>(string), on both IEventStoreOptions and EventGraph. There is a new trim-safe overload, RegisterTagType<TTag, TInner>(Func<TTag, TInner> unwrapper, string? tableSuffix = null), backed by TagTypeRegistration.FromUnwrapper. Marten builds with no IL2026/IL3050 from tag registration. The GetMethod(nameof(TagTypeRegistration.Create)) auto-discovery lookup is unchanged and still resolves.

Tests

All runs are local against Postgres on :5432.

  • stream_tombstoning_compliance (StreamTombstoningCompliance, SupportsTombstoneStream => true): 9/9.
  • New EventSourcingTests/Archiving/tombstoning_streams.cs: 11 facts.
    • Archived partitioning, Guid/string × single/conjoined, deleting both an archived and a live stream.
    • Tag rows in a partitioned tag table, registered through the new unwrapper overload.
    • UseTenantPartitionedEvents deletes only the session tenant.
    • Strict identity enforcement releases the id.
    • Projected documents are left alone.
    • The wrong-identity overload is refused.
    • A docs sample.
  • Both of the above: 20/20 on net10.0 and on net9.0.
  • DCB, tag, archiving and strict-identity tests: 316 passed, 7 skipped, 0 failed.
  • Compliance, Aggregation, FetchForWriting, appending and Bugs: 1430 passed, 22 skipped, 0 failed.
  • src/Marten.slnx: builds with 0 errors.

Docs

  • docs/events/archiving.md: new section "Deleting a Stream Permanently", with a snippet sample and a comparison table against ArchiveStream. It covers queueing until SaveChangesAsync, tag rows, tenant scoping, the missing-stream no-op, partition coverage, sequence numbers not being reused, and projected documents being left alone.
  • docs/events/dcb.md: new section on registering tag types under trimming or Native AOT.

Found along the way (not fixed here)

Archiving a tagged stream under UseArchivedStreamPartitioning fails with an FK violation. mt_archive_stream moves the events to the archived partition but not the tag rows that reference them (mt_event_tag_*_seq_id_is_archived_fkey). This is unrelated to this change and is noted in the new test. It probably deserves its own issue.

🤖 Generated with Claude Code

https://claude.ai/code/session_01J8KxC18Q51z5w4u6YARwe4

Bumps the JasperFx family to 2.90.0 and implements
IEventStoreOperations.TombstoneStream(Guid|string) as DeleteStreamOperation,
a queued single-statement delete of the stream's tag rows, events and stream
row (plus the mt_streams_identity row under strict identity enforcement),
scoped to the session tenant under conjoined tenancy. The event sequence is
never touched. Fills the EventAuxiliaryOperations.TombstoneStream slot in
PostgresEventStoreDialect.

Enrolls StreamTombstoningCompliance, flips SupportsTombstoneStream, and adds
Marten-specific tests for archived and tenant partitioning, strict identity
enforcement, projected documents and the identity mismatch guard.

Carries #1018's RequiresUnreferencedCode/RequiresDynamicCode up to
RegisterTagType<TTag> and adds the AOT-safe RegisterTagType<TTag, TInner>
(unwrapper) backed by TagTypeRegistration.FromUnwrapper.

Documents TombstoneStream in docs/events/archiving.md and the AOT tag
registration in docs/events/dcb.md.

Closes #5673

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J8KxC18Q51z5w4u6YARwe4
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit 8689bc8 into master Oct 15, 2026
32 checks passed
@jeremydmiller
jeremydmiller deleted the GH-5673-tombstone-stream branch October 15, 2026 17:10
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.

Implement IEventStoreOperations.TombstoneStream (hard stream delete) and enroll StreamTombstoningCompliance

1 participant