docs(historian): #691 bounding values (ReturnBounds) — Historian.md + CLAUDE.md

This commit is contained in:
Joseph Doherty
2026-10-12 16:27:27 -04:00
parent 92aab7282c
commit f6681544e7
2 changed files with 40 additions and 6 deletions
+39 -6
View File
@@ -752,10 +752,43 @@ data.
asking for `NumValuesPerNode + m + 16` (m = the pre-window rows the first read saw, unclamped). Only if that re-read is still
short because of pre-window rows does the node fail `BadTooManyOperations` (not a backend fault) rather than answer a short page
that would end the client's paging early. Every gateway stream is still drained to completion.
- **`ReturnBounds` is not honoured** — on any backend: the server does not pass it to the data source, so no bounding values are
returned (see [Known limitations](#known-limitations)). The AVEVA boundary row is not used as a bound either: it is stamped at the
query start rather than carrying the time of the real prior sample, so it could only ever be a Part 11 *interpolated* bound, which
this backend does not compute.
- **`ReturnBounds` is served by the server, not by this row** (#691, [Bounding values](#bounding-values-returnbounds-691)). The AVEVA
boundary row is never used as a bound: it is stamped at the query start rather than carrying the time of the real prior sample, so
it could only ever be a Part 11 *interpolated* bound. The start bound on the Gateway is the real prior sample, found by the
#686 walk-back.
### Bounding values (`ReturnBounds`, #691)
⚠️ **Client-visible.** Until #691 `ReadRawModifiedDetails.ReturnBounds` was ignored on every backend. It is now honoured on
**every** backend (SqlServer, QuestDB, Gateway, and none), following OPC UA Part 11 §4.6 / §6.5.3.2 and checked against every row
of Part 11 Table 1. **Note that the OPC Foundation .NET SDK's `new ReadRawModifiedDetails()` defaults `ReturnBounds` to `true`**:
a .NET client that never set it used to get stored values only and now gets bounds too. `Client.Shared` (Client.CLI / Client.UI)
sets it `false` explicitly, and ScadaBridge does not issue HistoryRead.
- **What a bound is.** Forward (`StartTime < EndTime`): the start bound is the stored sample **at or immediately before**
StartTime, the end bound the one **at or immediately after** EndTime. Reverse mirrors it (start bound at or after StartTime, end
bound at or before EndTime), newest first. A sample exactly **on** StartTime is the bound and is already the data's first value
(nothing extra is added); a sample exactly on EndTime is added as the end bound (the data window excludes EndTime).
`StartTime == EndTime` reads forward: the data is the samples at that instant, the end bound the first sample strictly after it.
- **A bound is a stored sample of any quality** — a Bad sample or a comms-loss outage marker comes back as stored. Inside a tie
cluster the bound is the cluster's first sample in the read's direction.
- **Missing bound:** `Bad_BoundNotFound`, null value, stamped with StartTime / EndTime. A one-sided read's open side (only a
StartTime, or only an EndTime) is never looked up; it ends the read as `Bad_BoundNotFound` stamped one second after (forward) or
before (reverse) the previous returned value (Table 1 footnotes a/b). `Bad_BoundNotSupported` is never returned — every backend
can find a bound.
- **Counting and paging.** Bounds **count toward `NumValuesPerNode`** (§6.5.3.2: "any Bounding Values returned are included in the
numValuesPerNode count"; `NumValuesPerNode = 1` returns the start bound alone). The start bound leads the **first** page and the
end bound ends the **last**: the pending end bound rides the continuation point, and when a page is exactly full the end bound is
the whole next page. Paging a bounded read at any page size concatenates to the unpaged answer.
- **How it is served — one authority, `HistoryBounds` in the OPC UA server.** "At or before T" is a newest-first read of `(0, T]`
capped at one — native on SqlServer/QuestDB, the #686 walk-back of bounded forward reads on the Gateway (so a tag with no sample
before StartTime walks back to 1970: the same ~14–16 small reads as a deep reverse page-through). "At or after T" is a forward read
of `[T, MaxValue)` capped at one; "strictly after" starts one storage resolution later. On QuestDB, whose window bounds truncate to
the microsecond, "on the bound" compares at that resolution, so a sample at ⌊StartTime⌋ is never returned twice. A first page
therefore costs up to three backend reads (start bound, data, end bound). The `BoundingValueConformance` suite pins those exact
lookup reads on SqlServer and QuestDB.
- **Modified reads:** `IsReadModified = true` with `ReturnBounds = true` is `Bad_InvalidArgument` per node (Part 11 §6.5.3.3).
- **Not affected:** Processed, AtTime and Events reads (they have no `ReturnBounds`).
### Continuation-point paging (Events) (#669)
@@ -1095,8 +1128,8 @@ whatever the SDK sets — #682 touches only the i=11201 folder (follow-up candid
sample per requested timestamp, so the single-shot backend returns the complete result in one
read and there is no "full page ⇒ maybe more" signal to page on. Returning the full result with
no continuation point is spec-conformant.
- **`ReturnBounds` is ignored** (every backend): Raw reads return stored values only; no bounding values are added at the window
edges. The node manager does not pass the flag to `IHistorianDataSource`.
- **Bounds are raw only:** `ReturnBounds` (#691, [Bounding values](#bounding-values-returnbounds-691)) never interpolates a bound —
it is always a stored sample or `Bad_BoundNotFound`.
- **No modified-value history on the `Gateway` backend.** Modified reads (`IsReadModified = true`) are served by
`SqlServer` and `QuestDb` ([Modified-value history](#modified-value-history-part-11), #668); the HistorianGateway exposes no
modified/edited-history surface, so on `Gateway` (and with no historian) they return `BadHistoryOperationUnsupported`.