fix(sql): attribute a duplicated key to its own tags; key-value cap gets 2x headroom (R06:S-1 review I-1, m-3)

One stray duplicate row used to fail every tag in a key-value group or none of them,
depending on whether an unrelated key was absent. Duplicated keys are now refused
per key: only that key's tags publish BadConfigurationError and the rest of the group
reads normally. The key-value cap reads up to 2x the distinct keys (+1 to detect
excess), so a few stray duplicates are attributed per key and only a gross excess
(history/events table) fails the whole group. Duplicated-key refusals count as
answered refusals for health. The row-cap, duplicate-key and multi-row warnings now
use the per-plan limiter (m-3); the reader-wide limiter is removed. Unit tests pin
both the all-keys-present and absent-key cases with Good controls; live SQL Server
gate added (23/23).

Refs #744
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 16:32:23 -04:00
parent 8f0046ed05
commit c1b9c7ec5c
8 changed files with 206 additions and 85 deletions
@@ -116,6 +116,69 @@ public sealed class SqlServerReadTests
}
}
/// <summary>
/// Review I-1, live: one stray duplicate row in a key-value table fails ONLY that key's tag; the other
/// key in the same group (one capped <c>SELECT TOP (5) … IN (@k0, @k1)</c>) reads Good — the positive
/// control — and the driver stays Healthy.
/// </summary>
[Fact]
public async Task ReadAsync_realSqlServer_aDuplicatedKeyFailsOnlyItsOwnTag()
{
SkipUnlessLive();
const string table = "RowCapCurrent";
var qualified = $"{SqlPollServerFixture.Schema}.{table}";
await using (var arrange = await _sql.OpenInspectionConnectionAsync())
{
await using var command = arrange.CreateCommand();
command.CommandText = $"""
IF OBJECT_ID(N'{qualified}') IS NOT NULL DROP TABLE {qualified};
CREATE TABLE {qualified} (
{SqlPollServerFixture.KeyColumn} nvarchar(64) NOT NULL,
{SqlPollServerFixture.ValueColumn} float NULL,
{SqlPollServerFixture.TimestampColumn} datetime2 NOT NULL);
INSERT INTO {qualified} VALUES
(N'Pump7', 1.0, '2026-07-24T10:00:00'),
(N'Pump7', 2.0, '2026-07-24T10:00:01'),
(N'Pump8', 8.5, '2026-07-24T10:00:00');
""";
await command.ExecuteNonQueryAsync(TestContext.Current.CancellationToken);
}
try
{
await using var driver = await StartAsync(
KeyValueOn("Sql/Current/Pump7", "Pump7", table),
KeyValueOn("Sql/Current/Pump8", "Pump8", table));
var snapshots = await driver.ReadAsync(
["Sql/Current/Pump7", "Sql/Current/Pump8"], TestContext.Current.CancellationToken);
snapshots[0].StatusCode.ShouldBe(SqlStatusCodes.BadConfigurationError);
snapshots[1].StatusCode.ShouldBe(SqlStatusCodes.Good);
Convert.ToDouble(snapshots[1].Value).ShouldBe(8.5);
driver.GetHealth().State.ShouldBe(DriverState.Healthy);
}
finally
{
await using var cleanup = await _sql.OpenInspectionConnectionAsync();
await using var drop = cleanup.CreateCommand();
drop.CommandText = $"DROP TABLE IF EXISTS {qualified}";
await drop.ExecuteNonQueryAsync(CancellationToken.None);
}
}
private static RawTagEntry KeyValueOn(string rawPath, string key, string table) => Tag(rawPath, new JsonObject
{
["driver"] = "Sql",
["model"] = "KeyValue",
["table"] = SqlPollServerFixture.Qualified(table),
["keyColumn"] = SqlPollServerFixture.KeyColumn,
["keyValue"] = key,
["valueColumn"] = SqlPollServerFixture.ValueColumn,
["timestampColumn"] = SqlPollServerFixture.TimestampColumn,
});
// ---- key-value model ----
/// <summary>The plan's headline case: a seeded key-value row round-trips as a Good snapshot.</summary>