fix(historian): #687 Gateway raw reads drop AVEVA's synthetic start-boundary row

An AVEVA Full raw read leads with a row stamped at the query start (the value in
effect, Quality=133, or a no-data NULL row); it was returned as a stored sample.
Raw reads now use the #681 classification (RawAggregateReader.RequestStartFor /
IsWindowSample — one authority, by timestamp, since a stored NULL looks the same
on the wire): request from 1 ms early with NumValuesPerNode + 16 rows, keep rows
at or after StartTime, drain the whole stream. A full page still holds exactly
the cap, so #669 paging/resume/tie-cluster reads are unchanged; a margin that
swallows the allowance fails the node rather than ending paging early.
ReturnBounds is not plumbed on any backend (documented).
This commit is contained in:
Joseph Doherty
2026-10-12 14:35:20 -04:00
parent 44a03bedc0
commit 9e47532283
6 changed files with 332 additions and 15 deletions
+28
View File
@@ -673,6 +673,32 @@ are disposed when the session closes). Resuming an unknown / evicted / released
`BadContinuationPointInvalid`. `releaseContinuationPoints` drops the stored cursors without reading
data.
### Gateway raw reads return stored samples only (#687)
> ⚠️ **Client-visible (#687).** An AVEVA `Full` raw read starts with a synthetic row stamped exactly at the **query start**: the
> value in effect there (AVEVA `Quality = 133`, the previous sample's value and quality — a value never stored at that time) or,
> before the tag's first sample, a NULL row (value 0, Bad, `Quality = 1`). The `Gateway` backend used to return that row as a stored
> sample: a window with no stored sample in it answered one fabricated value stamped at StartTime (live: `[18:06:21, 18:06:39]` ⇒
> `18:06:21 v=30 Good`), a window before the tag's history answered `v=0 Bad` instead of `GoodNoData`, and a start-only read led
> with the fabricated row. It is now dropped; such windows read `GoodNoData`.
- **How.** The same classification the raw-evaluated aggregates use (#681, `RawAggregateReader.RequestStartFor` /
`IsWindowSample` — one authority): the gateway is asked from **1 ms before** the window, so the boundary row lands before it, and
only rows stamped at or after StartTime are returned. The decision is by **timestamp**, not by value or quality — a stored NULL
sample looks exactly like the no-data boundary row on the wire (value 0, Bad), and a stored NULL inside the window is still
returned (Bad), as it is a stored sample.
- **Paging (#669) is unaffected.** The gateway is asked for `NumValuesPerNode + 16` rows (`NumValuesPerNode = 0` stays unlimited),
so the boundary row cannot shorten a page: a full page still holds exactly `NumValuesPerNode` stored samples and carries a
continuation point, and a resumed page (which re-reads from the last returned timestamp inclusive, and the tie-cluster over-fetch
`start == end` read) no longer gets a fabricated row at its resume point. If more than 16 rows lie in the 1 ms before the window
and exhaust that allowance, the page cannot be read completely and the node fails (`HistoryReadLimitExceededException`, not
counted as a backend fault) rather than answering 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.
### Continuation-point paging (Events) (#669)
A capped event read (`NumValuesPerNode > 0`) pages exactly like Raw, with one difference: the trigger is the **source's
@@ -964,6 +990,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`.
- **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`.