Skip to content

Implement IEventDatabase.DeleteProjectionProgressByShardNameAsync (#4785)#4786

Merged
jeremydmiller merged 3 commits into
masterfrom
feat/4785-delete-progress-by-shard-name
Jun 25, 2026
Merged

Implement IEventDatabase.DeleteProjectionProgressByShardNameAsync (#4785)#4786
jeremydmiller merged 3 commits into
masterfrom
feat/4785-delete-progress-by-shard-name

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Summary

  • Bumps JasperFx + JasperFx.Events (+ source generators) 2.15.0 → 2.16.0, which adds the new store-agnostic abstraction IEventDatabase.DeleteProjectionProgressByShardNameAsync (WritePatchByType #473 / WritePatchByType - inject into writer stream #474). The default implementation throws NotSupportedException (silent no-op on a delete is dangerous), so Marten must override it.
  • Adds the override on MartenDatabase: queues DeleteProjectionProgress(Events, shardIdentity) against mt_event_progression via a lightweight session (AllowAnyTenant = true so per-tenant identities pass through). The match is on the exact name value — bypassing the Projections.All lookup that IEventStore<,>.DeleteProjectionProgressAsync uses (and that throws ArgumentOutOfRangeException for an unregistered name).
  • A non-existent identity is a clean no-op (zero rows affected, no throw).
  • Per-tenant scoping flows through the identity itself: a {Name}:{ShardKey}:{tenantId} 3-segment identity deletes only that one tenant's row.

This is the eject path for an orphan shard — one whose projection has been renamed/versioned/removed since the row was written, or that was left behind by a topology change. Today the registered-projection-keyed delete throws in exactly that scenario.

Unblocks CritterWatch#476 Part 1 (operator "Eject Shard" action). Polecat needs the symmetric override (separate issue).

Test plan

  • Bug_4785_delete_projection_progress_by_shard_name (4 tests, all green locally on net10):
    • deletes_an_orphan_progression_row_by_raw_shard_identity — row written via raw SQL with no matching projection registered; delete drops it.
    • non_existent_identity_is_a_clean_no_op — unknown identity does not throw; nothing collateral touched.
    • only_the_matching_identity_is_deleted — three sibling rows seeded; only the named one is removed.
    • per_tenant_identity_deletes_only_that_tenants_row:tenantId-suffixed identity scopes the delete to one tenant.
  • CI matrix (Postgres 15/latest × net8/net10 × Newtonsoft/STJ).

Closes #4785

🤖 Generated with Claude Code

jeremydmiller and others added 3 commits June 25, 2026 12:44
)

Override the store-agnostic abstraction added in JasperFx.Events 2.16.0
(#473 / #474) so Marten can drop a single
mt_event_progression row by its raw ShardName.Identity, bypassing the
registered-projection lookup that DeleteProjectionProgressAsync goes
through (and that throws ArgumentOutOfRangeException for an unregistered
name). This is the eject path for an orphan shard whose projection has
been renamed/versioned/removed since the row was written.

- Bump JasperFx + JasperFx.Events + sourcegens 2.15.0 -> 2.16.0
- Override on MartenDatabase queues a DeleteProjectionProgress against
  the EventGraph progression table via a lightweight session
  (SessionOptions.AllowAnyTenant so per-tenant identities pass through);
  non-existent identity is a clean no-op
- Per-tenant scoping flows through the identity itself (the trailing
  :tenantId on the 3-segment {Name}:{ShardKey}:{tenantId} grammar)
- Bug_4785 regression test covers: orphan delete, no-op on unknown name,
  isolation across siblings, per-tenant identity scoping

Unblocks CritterWatch#476 Part 1 (operator "Eject Shard" action).

Closes #4785

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds an exhaustive permutation walk over every distinct Identity shape the
ShardName ctors / Compose() can produce, pinning both the JasperFx grammar
contract (expected literal per input) and the exact-match WHERE clause in
DeleteProjectionProgress.

Permutations covered:
- 2-segment Name:ShardKey (version=1, no tenant) — default-shardKey ctor
  variant separated as its own Fact since it's a distinct entry point
- 3-segment Name:V{n}:ShardKey (version>1, no tenant)
- 3-segment Name:ShardKey:tenant (version=1, tenant set)
- 4-segment Name:V{n}:ShardKey:tenant (version>1, tenant set)
- HighWaterMark literal (ctor collapses Identity to the constant)
- All grammars seeded together + targeted delete confirms zero collateral

Also confirms Compose() produces the same Identity as the 4-arg ctor for
each row. 15 tests total, all green on net10.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Strengthen Bug_4785 coverage around the version+tenant axis the user
flagged. Two new tests:

- deleting_one_does_not_touch_a_sibling_that_differs_by_one_axis —
  Theory walking every "differ by ONE axis" pair (version-only,
  tenant-only, shardKey-only, name-only, AND a combined version+tenant
  pair). Asserts the exact-match WHERE clause never picks up a sibling
  by prefix, normalization, or accident. Catches any future regression
  if the delete path ever grew a fuzzy match.
- writer_and_deleter_agree_on_identity_for_versioned_per_tenant_shard
  — drives an InsertProjectionProgress through a real lightweight
  session for a 4-segment Name:V{n}:ShardKey:tenant identity, then
  deletes via the override. Proves byte-for-byte agreement between the
  real Marten writer and the new delete contract when BOTH knobs are on.

24 tests, all green on net10.

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.

Implement IEventDatabase.DeleteProjectionProgressByShardNameAsync (raw-identity progression delete, bypass Projections.All)

1 participant