fix(sql): row-cap key-value and where-pair plans with a TOP/LIMIT N+1 dialect seam (R06:S-1)

Key-value and where-pair plans had no row limit, so a selector pointed at a history
or events table materialised every matching row on every poll. ISqlDialect gains
RowLimitPrefix/RowLimitSuffix(n) (SQL Server TOP (n), Postgres LIMIT n, Oracle FETCH
FIRST n ROWS ONLY); plans carry a RowCap (distinct keys, or 1) and ask for RowCap+1
rows; the reader also stops client-side at RowCap+1. Getting the extra row proves the
selector ambiguous: the group publishes BadConfigurationError with a rate-limited
warning. The reader reports those answered refusals so the driver counts them as
answer evidence, and any refusal-only poll now restores Healthy after a comms fault.

Refs #744
Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT
This commit is contained in:
Joseph Doherty
2026-10-22 15:57:13 -04:00
parent 7f5d959fb9
commit 0a5aa9fc43
21 changed files with 420 additions and 54 deletions
+11
View File
@@ -166,6 +166,17 @@ 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.
⚠️ **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
looks higher than expected.