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
+17 -10
View File
@@ -166,16 +166,23 @@ emits a `SqlQueryPlan` per distinct group key. Grouping is deterministic (groups
order of their first member, members keep their input order, parameters keep first-appearance order),
so an unchanged configuration produces byte-identical SQL.
**Every key-value and where-pair query is row-capped (R06:S-1).** A key-value plan asks for one row more
than it has distinct keys (`TOP (n+1)` / `LIMIT n+1` / `FETCH FIRST n+1 ROWS ONLY`), a where-pair wide-row plan
for two, and the reader stops reading there even if a provider ignores the limit. Receiving the extra row
proves the selector is ambiguous — a history/events table, or a where column that is not unique — so every
member of that group publishes `BadConfigurationError` with a rate-limited warning naming the table and
selector, instead of the old behaviour of materialising every matching row on every poll and publishing an
arbitrary "last row" at Good. A duplicate that fits inside the cap (a duplicated key alongside an absent one)
still resolves last-row-wins. `topByTimestamp` keeps its single-row limit. An over-cap answer, like every
other refusal (absent row, NULL cell, uncoercible cell), is the database answering: it advances
`LastSuccessfulRead` and restores `Healthy` after a comms fault.
**Every key-value and where-pair query is row-capped (R06:S-1).** A key-value plan reads at most twice as
many rows as it has distinct keys and asks for one more (`TOP (2n+1)` / `LIMIT 2n+1` / `FETCH FIRST 2n+1 ROWS
ONLY`); a where-pair wide-row plan asks for two. The reader stops there even if a provider ignores the limit.
- **A duplicated key is refused per key.** When a key comes back more than once, only the tags on THAT key
publish `BadConfigurationError`; every other key in the group reads normally, whatever else is present or
absent. (Before this, a duplicate was last-row-wins Good.)
- **A gross excess fails the group.** Receiving the extra row past the cap means a history/events table or a
non-unique where column, which cannot be attributed because the rest is unread, so every member of the group
publishes `BadConfigurationError`. For a where-pair any second row is this case.
- Each case logs a per-plan rate-limited warning naming the table and the selector (or an example key).
`topByTimestamp` keeps its single-row limit.
- **Upgrade note:** a deployment whose key-value source carries stray duplicate rows, or whose where column is
not unique, will see those tags go Bad where they used to read an arbitrary row at Good.
Every such refusal, like the other refusals (absent row, NULL cell, uncoercible cell), is the database
answering: it advances `LastSuccessfulRead` and restores `Healthy` after a comms fault.
⚠️ **Group keys are case-sensitive on identifiers.** Two tags authored `dbo.T` and `DBO.T` plan as
**two** groups — one extra round-trip, not a correctness problem, but worth knowing if a group count