docs(code-review): close Driver.Sql-002 + Driver.Sql.Dialect-001/002/003

Dialect header 3 -> 0 open. The Driver.Sql header count is left alone for the
orchestrator to reconcile — sibling agents are editing that file in parallel.
This commit is contained in:
Joseph Doherty
2026-08-10 08:04:40 -04:00
parent 401d670639
commit 1fe0017600
2 changed files with 17 additions and 9 deletions
+8 -2
View File
@@ -65,13 +65,19 @@ surface.
| Severity | Medium |
| Category | Error handling & resilience |
| Location | `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Sql/SqlGroupPlanner.cs:127` |
| Status | Open |
| Status | Resolved |
**Description:** `BuildKeyValuePlan` emits one bind parameter per distinct `keyValue` in a group, with no upper bound and no chunking. Every provider caps this: `Microsoft.Data.SqlClient` allows 2100 parameters per command, and Oracle limits an `IN` expression list to 1000 entries. A key-value (EAV) staging table is precisely the shape that attracts thousands of tags on one table — which is one group by the group-key rule — so above the cap the provider rejects the statement after the connection opened, `QueryAsync` returns `GroupResult.Failed`, and every tag in that group publishes `BadCommunicationError` on every poll, permanently. The failure is also mis-diagnosed: `ObservePollOutcome` classifies `BadCommunicationError` as connection-class, so the driver reports Degraded and the host `Stopped` (see Driver.Sql-005), pointing the operator at the network rather than at a tag count. Nothing upstream caps the tag count per driver or per table, and the log line names the SQL but not the cause.
**Recommendation:** Chunk the `IN` list. The natural shape is a `MaxBindParameters` member on `ISqlDialect` (see Driver.Sql.Dialect-002) and a planner that emits several plans for one group key when the member count exceeds it — `SqlPollReader.Slice` already maps per `plan.Members`, so several plans sharing a group key need no reader change. Failing that, gate the tag count at deploy time with a message naming the provider limit, rather than letting it surface as a permanent per-poll communication error.
**Resolution:** _(open)_
**Resolution (2026-08-10):** Fixed the recommended way, in the same commit as its seam half Driver.Sql.Dialect-002 (`886ce1b8`) — the deploy-time tag-count gate was rejected as refusing a configuration the driver can serve perfectly well. `SqlGroupPlanner.AddKeyValuePlans` splits a key-value group's distinct keys into chunks of `dialect.MaxBindParameters` and emits one plan per chunk, all carrying that group's key. Three decisions worth knowing:
- **Members follow their key, never their position.** A member placed in a chunk that does not bind its own key would read a result set that cannot contain its row and publish `BadNoData` forever — a quieter failure than the one being fixed. Two tags on one `keyValue` therefore land in the same chunk (one bound parameter, both fed), pinned by `MembersSharingAKeyValue_stayInTheSameChunk` and `EveryMemberLandsInTheChunkThatBindsItsOwnKey`.
- **`SqlPollReader` is untouched**, as the recommendation predicted: it runs one command per plan and slices each plan's own `Members` back. That claim is not asserted by reading — `ThePollReader_readsEveryTagCorrectly_whenOneGroupIsSplitAcrossSeveralPlans` drives the real read path over the SQLite fixture at a ceiling of **1**, so three keys become three commands, and every tag still gets its own value (and the absent key still `BadNoData`).
- **The load-bearing test is the falsifiability control**, `AGroupAtOrUnderTheCeiling_stillEmitsExactlyOnePlan` (1/2/3 keys under a ceiling of 3): a planner that split eagerly, or off a stale count, passes every "it splits" assertion above while silently turning one round-trip per source into N. A dialect reporting a ceiling below 1 is rejected loudly rather than clamped — chunking to zero loops forever, and a one-per-chunk fallback would hide a broken dialect behind a poll that suddenly costs a round-trip per tag.
RED evidence: with the shipped `OracleDialect` (1000-entry `IN` limit) and 1001 distinct keys, `plans.Count should be 2 but was 1` before the change; green after. The mis-diagnosis this finding also describes — `BadCommunicationError` classified as connection-class — belongs to Driver.Sql-005 and is untouched here; chunking removes the input that produced it on this path.
---