diff --git a/Directory.Packages.props b/Directory.Packages.props
index b69b2c65ce..fa1fb11344 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -105,14 +105,16 @@
JasperFx 2.28.1: jasperfx#528 (marten#4966) — natural-key discovery now builds an event
mapping for a two-arg static evolve method (static TDoc Apply(TEvent e, TDoc current)), so a
natural key that CHANGES via an update event is written to mt_natural_key_X on live append and
- rebuild; previously only the create event's key was ever recorded. -->
-
-
-
+ rebuild; previously only the create event's key was ever recorded.
+ JasperFx 2.29.0: jasperfx#529 (marten#4975) — exact ReadProjectionProgressAsync(ShardName) overload
+ on IEventDatabase (no version/shard collapsing), adopted by MartenDatabase here. -->
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
diff --git a/src/DaemonTests/Internals/read_projection_progress.cs b/src/DaemonTests/Internals/read_projection_progress.cs
index 75d7fe54f6..203e06f893 100644
--- a/src/DaemonTests/Internals/read_projection_progress.cs
+++ b/src/DaemonTests/Internals/read_projection_progress.cs
@@ -44,6 +44,10 @@ private async Task seedProgression(params (ShardName name, long sequence)[] rows
((IEventDatabase)theStore.Tenancy.Default.Database)
.ReadProjectionProgressAsync(projectionName, tenantId, CancellationToken.None);
+ private ValueTask read(ShardName name) =>
+ ((IEventDatabase)theStore.Tenancy.Default.Database)
+ .ReadProjectionProgressAsync(name, CancellationToken.None);
+
[Fact]
public async Task reads_the_store_global_row()
{
@@ -103,4 +107,43 @@ await seedProgression(
(await read("Orders", null))!.Sequence.ShouldBe(10);
(await read("OrdersHistory", null))!.Sequence.ShouldBe(5);
}
+
+ // #4975 / jasperfx#529 — the exact ShardName overload does NO collapsing.
+ [Fact]
+ public async Task exact_shard_name_reads_the_specific_version_not_the_newest()
+ {
+ await seedProgression(
+ (ShardName.Compose("Orders", version: 2), 25),
+ (ShardName.Compose("Orders", version: 3), 40));
+
+ var v2 = await read(ShardName.Compose("Orders", version: 2));
+
+ v2.ShouldNotBeNull();
+ v2.Sequence.ShouldBe(25); // the exact V2 row, not the newest V3
+ v2.ProjectionName.ShouldBe("Orders");
+ v2.AgentStatus.ShouldBeNull();
+ v2.LastHeartbeat.ShouldBeNull();
+ }
+
+ [Fact]
+ public async Task exact_shard_name_returns_null_when_the_identity_does_not_exist()
+ {
+ await seedProgression((ShardName.Compose("Orders"), 10));
+
+ (await read(ShardName.Compose("Orders", version: 9))).ShouldBeNull();
+ }
+
+ [Fact]
+ public async Task exact_shard_name_distinguishes_tenant_rows()
+ {
+ await seedProgression(
+ (ShardName.Compose("Orders"), 10),
+ (ShardName.Compose("Orders", tenantId: "tenant1"), 99));
+
+ (await read(ShardName.Compose("Orders")))!.Sequence.ShouldBe(10);
+
+ var tenantRow = await read(ShardName.Compose("Orders", tenantId: "tenant1"));
+ tenantRow!.Sequence.ShouldBe(99);
+ tenantRow.TenantId.ShouldBe("tenant1");
+ }
}
diff --git a/src/Marten/Storage/MartenDatabase.EventStorage.cs b/src/Marten/Storage/MartenDatabase.EventStorage.cs
index 4747936024..2d8c395c77 100644
--- a/src/Marten/Storage/MartenDatabase.EventStorage.cs
+++ b/src/Marten/Storage/MartenDatabase.EventStorage.cs
@@ -346,6 +346,46 @@ public async Task> AllProjectionProgress(string? tenan
}
}
+ ///
+ /// #4975 / jasperfx#529 — exact per-cell progression read. Unlike the
+ /// overload this does no
+ /// version/shard collapsing: it looks up the single mt_event_progression row whose name
+ /// equals 's verbatim, so a blue/green deploy's
+ /// versions, a sliced projection's shard keys, and per-tenant partitions each address their own row.
+ /// A of All is the projection's global cell. Returns null when no
+ /// row exists for that identity; and
+ /// stay null (jasperfx#519).
+ ///
+ public async ValueTask ReadProjectionProgressAsync(
+ ShardName name, CancellationToken token)
+ {
+ await EnsureStorageExistsAsync(typeof(IEvent), token).ConfigureAwait(false);
+
+ await using var conn = CreateConnection();
+ try
+ {
+ await conn.OpenAsync(token).ConfigureAwait(false);
+
+ var builder = new CommandBuilder();
+ builder.Append(
+ $"select last_seq_id from {Options.EventGraph.DatabaseSchemaName}.mt_event_progression where name = ");
+ builder.AppendParameter(name.Identity);
+
+ await using var reader = await conn.ExecuteReaderAsync(builder, token).ConfigureAwait(false);
+ if (!await reader.ReadAsync(token).ConfigureAwait(false))
+ {
+ return null;
+ }
+
+ var sequence = await reader.GetFieldValueAsync(0, token).ConfigureAwait(false);
+ return new ProjectionProgressRow(name.Name, name.TenantId, sequence, null, null);
+ }
+ finally
+ {
+ await conn.CloseAsync().ConfigureAwait(false);
+ }
+ }
+
///
/// #4785 / jasperfx#473 — Marten override of the store-agnostic
///