Sql browse-commit emits a rowSelector-less WideRow config that authoring and runtime both reject — commit now fails loudly #552

Closed
opened 2026-08-10 00:26:20 -04:00 by dohertj2 · 1 comment
Owner

Found (and verified live) by the #551 work. RawBrowseCommitMapper.BuildSqlTagConfig produces {"model":"WideRow","table":…,"columnName":…} with no rowSelector — a shape SqlTagConfigModel.Validate and SqlEquipmentTagParser both refuse. Such a tag has never been readable: before #551 the commit persisted it silently and the driver skipped it (BadNodeIdUnknown forever); since #551's service gate, the commit itself fails with an actionable message.

So the gate moved the failure to the right place, but Sql browse-commit is now a flow that always fails at commit and needs its own fix. Candidates:

  • the browse session supplies a rowSelector (e.g. a topByTimestamp choice — needs a timestamp-column pick in the browse/commit UI), or
  • Sql browse-commit routes through the typed editor for completion before persisting.

Every other browsable driver's commit-emitted config was verified passing the gate (MQTT both modes, MTConnect, and the rest) — Sql is the only offender; parity is pinned in RawTagConfigGateTests.

Found (and verified live) by the #551 work. `RawBrowseCommitMapper.BuildSqlTagConfig` produces `{"model":"WideRow","table":…,"columnName":…}` with **no `rowSelector`** — a shape `SqlTagConfigModel.Validate` and `SqlEquipmentTagParser` both refuse. Such a tag has **never been readable**: before #551 the commit persisted it silently and the driver skipped it (BadNodeIdUnknown forever); since #551's service gate, the commit itself fails with an actionable message. So the gate moved the failure to the right place, but **Sql browse-commit is now a flow that always fails at commit** and needs its own fix. Candidates: - the browse session supplies a `rowSelector` (e.g. a `topByTimestamp` choice — needs a timestamp-column pick in the browse/commit UI), or - Sql browse-commit routes through the typed editor for completion before persisting. Every other browsable driver's commit-emitted config was verified passing the gate (MQTT both modes, MTConnect, and the rest) — Sql is the only offender; parity is pinned in `RawTagConfigGateTests`.
Author
Owner

FIXED — merged to master as 1f351250 (branch commit 7498d39e).

The browse session now STATES the row selector; the mapper never infers (the MQTT Sparkplug rule):

  • SqlBrowseSession computes a per-table topByTimestamp candidate from the column metadata it already reads (_dialect.MapColumnType(DATA_TYPE) == DriverDataType.DateTime — type only, never name; all four dialects fold their date/time families onto that member). Tie-break on multiple candidates: first in catalog order, which every dialect's ListColumnsSql already guarantees (ORDER BY ORDINAL_POSITION etc.) — stable across calls/connections/servers. No extra INFORMATION_SCHEMA round-trip (pinned by a command-count test). Stated as a new AddressFields entry via the new shared SqlBrowseAddressFields constants (Driver.Sql.Contracts), so producer and consumer can no longer drift on the key by coincidence.
  • BuildSqlTagConfig writes rowSelector: { topByTimestamp: <candidate> }. The emitted blob is double-accepted in tests by SqlTagConfigModel.Validate/TagConfigValidator and the real SqlEquipmentTagParser, with a control asserting the pre-#552 bytes are refused by all three. The blob's top-level timestampColumn (source-timestamp) is deliberately NOT set — ordering by a column is not a claim that it timestamps the value.
  • No-candidate tables (no date/time column): the leaf is refused in words at the modal via the existing DescribeUncommittableLeaf seam — the message names the leaf, the table, and the fix (a whereColumn/whereValue selector, whose value is data no browse can supply). The mapper still emits the wide-row shape (not the generic address key) so the table/column survive into the typed editor; Sql_refusal_and_the_emitted_blob_agree pins the two layers in step both directions.

Falsified: candidate computation nulled → exactly the 3 candidate tests red; tie-break inverted → exactly 1 red; mapper stops writing the selector → 6 red. Verified on the merged tree: AdminUI 1262, Sql 363, Sql.Browser 79, ControlPlane 261 — all green. The RawTagConfigGateTests Sql leg is flipped from records-the-gap to passes-the-gate.

Residual found en route, filed separately: RawBrowseCommitMapper's fallback comment cites a RawBrowseCommitMapperParityTests guard that does not exist (#555).

**FIXED** — merged to master as `1f351250` (branch commit `7498d39e`). The browse session now STATES the row selector; the mapper never infers (the MQTT Sparkplug rule): - `SqlBrowseSession` computes a per-table `topByTimestamp` candidate from the column metadata it already reads (`_dialect.MapColumnType(DATA_TYPE) == DriverDataType.DateTime` — **type only, never name**; all four dialects fold their date/time families onto that member). Tie-break on multiple candidates: **first in catalog order**, which every dialect's `ListColumnsSql` already guarantees (`ORDER BY ORDINAL_POSITION` etc.) — stable across calls/connections/servers. No extra INFORMATION_SCHEMA round-trip (pinned by a command-count test). Stated as a new `AddressFields` entry via the new shared `SqlBrowseAddressFields` constants (`Driver.Sql.Contracts`), so producer and consumer can no longer drift on the key by coincidence. - `BuildSqlTagConfig` writes `rowSelector: { topByTimestamp: <candidate> }`. The emitted blob is double-accepted in tests by `SqlTagConfigModel.Validate`/`TagConfigValidator` **and** the real `SqlEquipmentTagParser`, with a control asserting the pre-#552 bytes are refused by all three. The blob's top-level `timestampColumn` (source-timestamp) is deliberately NOT set — ordering by a column is not a claim that it timestamps the value. - **No-candidate tables** (no date/time column): the leaf is refused **in words at the modal** via the existing `DescribeUncommittableLeaf` seam — the message names the leaf, the table, and the fix (a `whereColumn`/`whereValue` selector, whose value is *data* no browse can supply). The mapper still emits the wide-row shape (not the generic address key) so the table/column survive into the typed editor; `Sql_refusal_and_the_emitted_blob_agree` pins the two layers in step both directions. Falsified: candidate computation nulled → exactly the 3 candidate tests red; tie-break inverted → exactly 1 red; mapper stops writing the selector → 6 red. Verified on the merged tree: AdminUI 1262, Sql 363, Sql.Browser 79, ControlPlane 261 — all green. The `RawTagConfigGateTests` Sql leg is flipped from records-the-gap to passes-the-gate. Residual found en route, filed separately: `RawBrowseCommitMapper`'s fallback comment cites a `RawBrowseCommitMapperParityTests` guard that does not exist (#555).
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: dohertj2/lmxopcua#552