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
@@ -74,6 +74,31 @@ internal static class RawAggregateReader
/// <summary>Rows the earlier-data probe asks for (a possible boundary row plus the first stored sample).</summary>
internal const int ProbeRows = 2;
/// <summary>
/// The <c>ReadRaw</c> start for a window that begins at <paramref name="windowStartUtc"/>: <see cref="BoundaryMargin"/>
/// earlier, so the AVEVA start-boundary row — stamped exactly at the QUERY start, carrying the value in effect there
/// (<c>Quality = 133</c>) or, with no earlier data, a NULL row (value 0, Bad OPC quality) — lands BEFORE the window
/// (<see cref="IsWindowSample"/>). The one place both the raw-evaluated aggregates (#681) and a raw HistoryRead
/// (#687) decide what the request start is. No margin when the window starts within one margin of
/// <see cref="DateTime.MinValue"/>.
/// </summary>
/// <param name="windowStartUtc">The window's (inclusive) start.</param>
/// <returns>The start to request.</returns>
public static DateTime RequestStartFor(DateTime windowStartUtc) =>
windowStartUtc.Ticks > BoundaryMargin.Ticks ? windowStartUtc - BoundaryMargin : windowStartUtc;
/// <summary>
/// True when a row of a stream requested from <see cref="RequestStartFor"/> is a stored sample of the window that
/// begins at <paramref name="windowStartUtc"/>: it is stamped at or after the window start. A row before it is the
/// AVEVA boundary row (whatever its value or quality — a stored NULL looks the same on the wire, so the timestamp is
/// the evidence) or a stored sample inside the margin; a row without a timestamp is never a sample.
/// </summary>
/// <param name="raw">The gateway row.</param>
/// <param name="windowStartUtc">The window's (inclusive) start.</param>
/// <returns>Whether the row belongs to the window.</returns>
public static bool IsWindowSample(HistorianSample raw, DateTime windowStartUtc) =>
raw.Timestamp is { } ts && ts.ToDateTime() >= windowStartUtc;
/// <summary>True for the aggregates this reader serves: the per-sample families of <see cref="AggregateFamilies"/>.</summary>
/// <param name="aggregate">Requested aggregate.</param>
/// <returns>Whether the aggregate is evaluated from raw samples.</returns>
@@ -113,7 +138,7 @@ internal static class RawAggregateReader
{
if (plan.BucketCount == 0) return [];
var needsRange = AggregateEvaluator.NeedsDataRange(aggregate, plan);
var requestStart = plan.StartUtc.Ticks > BoundaryMargin.Ticks ? plan.StartUtc - BoundaryMargin : plan.StartUtc;
var requestStart = RequestStartFor(plan.StartUtc);
var maxValues = MaxValuesFor(maxWindowRows);
var buckets = new RawBucketAccumulator(aggregate == HistoryAggregateType.MaximumActualTime);