Commit Graph

8 Commits

Author SHA1 Message Date
Joseph Doherty 967e5140f1 feat(sql): SqlPollReader — grouped read, bounded deadline, ordered slice-back
Executes a poll pass: resolve refs -> SqlGroupPlanner.Plan -> one command per
group -> slice the result set back to per-tag DataValueSnapshots, positionally
aligned with the caller's reference list (design §3.2).

The frozen-peer contract (design §8.3, the R2-01 S7 lesson) is the core of it.
CommandTimeout is only the server-side backstop; the client-side bound is a
linked CancelAfter PLUS Task.WaitAsync, because a linked token bounds only a
provider that honours it and ADO.NET providers vary. The whole per-group
operation — waiting for a concurrency slot, OpenAsync, and the query — runs off
one operationTimeout budget, so ReadAsync returns on time regardless of what the
database is doing. A breach Bad-codes that group (BadTimeout); caller
cancellation propagates instead, since nobody consumes a torn-down poll.

Also: absent row (BadNoData) stays distinct from a NULL cell (Uncertain, or Bad
under nullIsBad); duplicate plan members are all fed (two tags on one keyValue
bind one parameter but stay two members); the concurrency slot is released by the
work, not the waiter, so a frozen database can never hold more than
maxConcurrentGroups connections; the whole call throws only when the connection
itself cannot open, which is what earns PollGroupEngine's backoff.

SqlStatusCodes is driver-local, matching every sibling driver's own table — there
is no shared helper in Core.Abstractions and drivers do not reference the OPC UA
SDK. Values were read off Opc.Ua.StatusCodes rather than copied from a sibling,
because two of those tables carry transcription errors.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-24 14:26:29 -04:00
Joseph Doherty 50f88b938f fix(sql): reject Unicode format chars in identifiers; stop overclaiming catalog validation
Three findings from the code review of the dialect seam:

- QuoteIdentifier used char.IsControl, which only covers Unicode category Cc.
  Bidi overrides, zero-width spaces, soft hyphen and BOM are category Cf and
  passed through untouched. They cannot escape the brackets, so this is not an
  injection bypass — but the method rejects control characters precisely to
  protect the integrity of logged/audited statements, and Cf characters spoof
  rendered text while comparing byte-different from the real catalog name
  (Trojan Source, CVE-2021-42574). Same rule, same rationale, wider category.

- The XML docs on both files asserted that identifiers 'are sourced only from
  catalog-validated (INFORMATION_SCHEMA) names' and that rejection is 'a
  backstop, not the primary defence'. Neither is true yet: design §8.1 does
  specify that gate, but nothing implements it and no task in the plan
  schedules one, so an authored TagConfig table/column reaches QuoteIdentifier
  unfiltered. The docs now say so, because under-scrutinising this method on
  the strength of a filter that does not exist is exactly the failure mode.

- The control-character test only pinned NUL, so a regression to a
  Contains('\0') check would still have passed. Pinned the whole Cc category
  plus the new Cf rule; verified load-bearing by deleting the guard and
  observing exactly the 5 new cases go red.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-24 14:07:54 -04:00
Joseph Doherty a05cff794c test(sql): SqliteDialect + poll fixture
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-24 14:04:06 -04:00
Joseph Doherty 6de782e0fd test(sql): cover the WideRow + rejection branches of the equipment-tag parser
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-24 13:59:01 -04:00
Joseph Doherty 3d7e1226d2 refactor(sql): promote the single-row limit onto ISqlDialect
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-24 13:57:25 -04:00
Joseph Doherty 984cc875e8 feat(sql): SqlGroupPlanner folds tags into one parameterized query per group
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-24 13:52:27 -04:00
Joseph Doherty 6bb0108d30 feat(sql): ISqlDialect + SqlServerDialect with golden catalog SQL
The dialect owns the two things ADO.NET's System.Data.Common base types
cannot abstract: identifier quoting and the metadata-catalog SQL. It is
the driver's SQL-injection boundary — values always bind as DbParameters,
identifiers cannot, so the few that reach a command text are catalog-
sourced and pass through QuoteIdentifier.

QuoteIdentifier brackets one identifier part and doubles embedded ] (the
only metacharacter that can terminate a bracketed identifier early);
rejects null/empty/whitespace, >128 chars (T-SQL sysname ceiling), and any
control character (incl. NUL) as ArgumentException, without echoing the
untrusted value into the message. MapColumnType folds the design §3.7
families case-insensitively and never throws — an unrecognised family
falls back to String so a browse over an exotic column still renders;
timestamp/rowversion and time are deliberately NOT mapped to DateTime.

Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-24 13:43:53 -04:00
Joseph Doherty 53ff041229 feat(sql): SqlTagDefinition + strict-enum equipment-tag parser
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
2026-07-24 13:36:51 -04:00