diff --git a/docs/plans/2026-10-09-pluggable-historian-design.md b/docs/plans/2026-10-09-pluggable-historian-design.md index 3ad29879..18b2d82a 100644 --- a/docs/plans/2026-10-09-pluggable-historian-design.md +++ b/docs/plans/2026-10-09-pluggable-historian-design.md @@ -199,8 +199,10 @@ next call (not latched). the first value's kind, provisioning later corrects it). Insert via `{s}.SampleRow` TVP with `INSERT … SELECT … WHERE NOT EXISTS`. Returns `true` on success, `false` on a SQL failure (the recorder's retry/quarantine owns retry). `ServerTimeUtc = SYSUTCDATETIME()`. *As built:* duplicate timestamps within one batch - collapse to the **first**; a `NaN`/`±Inf` `Double` is stored as `Empty` keeping its status (it cannot live in a SQL - `float`); a 2627/2601 duplicate-key error rolls the statement back, so the statement is re-run (≤ 2 retries) rather + collapse to the **first**; a `NaN`/`±Inf` `Double` is stored as `Empty` (it cannot live in a SQL `float`) and, per the + final-review I-1 ruling, **an `Empty` sample is always Bad**: a non-Bad non-finite value is stored `BadOutOfRange`, any + other non-Bad `Empty` `BadNoData`, an existing Bad code kept; reads clamp a legacy non-Bad `Empty` row to `BadNoData`; + tag names over 400 characters are refused, never truncated; a 2627/2601 duplicate-key error rolls the statement back, so the statement is re-run (≤ 2 retries) rather than treated as success. - `IHistorianProvisioning.EnsureTagsAsync`: `MERGE` upsert of every request (all types — no numeric filter on this backend). Per-request failures are counted, never thrown. @@ -273,7 +275,8 @@ public interface IHistorianDataUpdater Each call is one transaction (`MERGE … OUTPUT $action` against the TVP). A value with a null timestamp ⇒ `BadInvalidTimestamp` for that item. *As built:* a `NaN`/`±Inf` `Double` ⇒ `BadInvalidArgument` (rejected, not stored as `Empty` -as the live-value writer does); duplicate timestamps in one request — Insert: first applies, later `BadEntryExists`; Replace/Update: +as the live-value writer does), and so is a null value with a non-Bad status (I-1); no DataType check against the node (M-1, +decided: values are stored as sent); a `DeleteRawModified` with a `MinValue` bound ⇒ `BadInvalidArgument`; duplicate timestamps in one request — Insert: first applies, later `BadEntryExists`; Replace/Update: last wins, earlier ones share its code; DeleteAtTime: first reports, later `BadNoEntryExists`. The node manager answers a null item `BadInvalidArgument`, `MinValue` `BadInvalidTimestamp`, an untypable value `BadTypeMismatch`, a backend throw `BadInternalError` and a deadline `BadTimeout` (a failure is not "unsupported"). Update/Insert on an unknown tag creates the tag row (same as live