fix(historian): #680 lane B — reverse end-of-data Partial (strictly inside), drop dead per-sample Percent cases

Per-sample aggregates (all but Average) now set Partial on a reverse read's interval
that strictly contains the tag's latest sample — the SDK's reverse m_startOfData
(verified vs the SDK harness), matching lane A's time-weighted rule. NeedsDataRange
therefore applies in both directions for every per-sample aggregate but Average.
PercentGood/Bad left the per-sample evaluator (lane A moved them to the time-weighted
family); Evaluate refuses non-per-sample aggregates up front. Comments fixed.
This commit is contained in:
Joseph Doherty
2026-10-11 16:00:13 -04:00
parent dbfe2fc4de
commit b8344bb151
8 changed files with 75 additions and 75 deletions
@@ -33,7 +33,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer;
/// carries a value; an <c>Empty</c>-kind sample is an outage marker (comms-loss <c>BadNoCommunication</c>
/// or a driver's Bad publish) and is Bad whatever its code. Raw returns every sample as stored, outage
/// markers included. Aggregates exclude Bad samples from the math, report the interval Uncertain when
/// any non-Good sample is present, and count them in PercentBad. AtTime never interpolates across a Bad
/// any non-Good sample is present, and count their duration as Bad time in PercentBad. AtTime never interpolates across a Bad
/// neighbour and reports a time inside an outage with the outage's Bad code (see <see cref="ReadAtTimeAsync"/>).
/// <b>An Empty sample is always served Bad:</b> a row with no value whose stored status is not Bad (only
/// possible for rows written before the writer forced Empty samples Bad) is read back as <c>BadNoData</c>,
@@ -405,14 +405,14 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
/// returned latest first (<see cref="BucketPlan"/>).
/// </para>
/// <para>
/// Per interval: no samples ⇒ <c>BadNoData</c>. PercentGood / PercentBad ⇒ <c>100 × good|bad / all</c> over
/// every sample, status Good. Otherwise no usable sample ⇒ <c>BadNoData</c>; a numeric aggregate with a usable
/// String/DateTime sample ⇒ <c>BadAggregateInvalidInputs</c>; else the value, Good when every sample is Good,
/// <c>UncertainDataSubNormal</c> when any is Bad or Uncertain. Every status — including <c>BadNoData</c> —
/// carries the Calculated info bits. Timestamps are the bucket start (its later edge on a reverse read), except
/// Minimum/MaximumActualTime (the sample's time; ties ⇒ earliest). On a forward read every aggregate but Average
/// sets the Partial bit on the bucket the tag's earliest sample lies strictly inside (#677) or its latest sample
/// lies in (#680). #680: Count counts Good samples (value-based status; 0/Good on an empty bucket inside the tag's
/// Per-sample aggregates, per interval: no samples ⇒ <c>BadNoData</c>. Otherwise no usable sample ⇒
/// <c>BadNoData</c>; a numeric aggregate with a usable String/DateTime sample ⇒ <c>BadAggregateInvalidInputs</c>;
/// else the value, Good when every sample is Good, <c>UncertainDataSubNormal</c> when any is Bad or Uncertain.
/// Every calculated status — including <c>BadNoData</c> — carries the Calculated info bits. Timestamps are the
/// bucket start (its later edge on a reverse read), except Minimum/MaximumActualTime (the sample's time; ties ⇒
/// earliest) and Start/End. Every aggregate but Average sets the Partial bit on the bucket the tag's earliest
/// sample lies strictly inside (forward, #677) or its latest sample lies in (#680; reverse: strictly inside).
/// PercentGood / PercentBad are duration-based since #680 (time-weighted family, below). #680: Count counts Good samples (value-based status; 0/Good on an empty bucket inside the tag's
/// data range), Start/End are the raw first/last sample of any quality, Delta is Uncertain only when the bucket's
/// first or last sample is not usable (see <see cref="AggregateEvaluator"/>).
/// </para>