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 ///