feat(historian): serve DurationGood/DurationBad on the SQL backends; pin the #821 late-start facts (#820, #821)

#820: SqlServer + QuestDB now serve Part 13 DurationGood / DurationBad from the
same Good / Bad region totals TimeWeightedAggregator already computes for
PercentGood / PercentBad — reported in milliseconds (OPC UA Duration, the SDK's
unit), Uncertain time in neither (Mantis 0006166), same BadNoData / status /
Partial rules. AggregateFamilies.Served grows 17 -> 19, so i=11201 and the
up-front refusal follow automatically; Gateway still refuses both (explicit
served set, unchanged). Client.Shared AggregateType / mapper / CLI gain both.
The #796 oracle answers DurationGood through the Good-only subclass as well.
QualityMix conformance asserts both directly (Q and QP, hand values, ms, and
= PercentGood x interval), replaces the "refused up front" test, and adds
Duration* to the forward/reverse/truncated oracle comparisons.

#821 (facts only, no decision): SdkAggregateParityTests pins an Uncertain-first
late-start interval (QP over [0,20), both directions) against ours (Part 13
Total 163.96, TimeAverage 10.93), the frozen 1.5.378.182 calculators (188.96,
9.448), SDK 2.0.1's stock calculators (188.96, 9.448) and a Good-first control
(all 163.96); LiveAggregateLateStartFactsTests pins the same through the booted
server's runtime AggregateManager (live AggregateFilter) incl. DurationGood
15000 / PercentGood 75 there vs 8000 / 40 in HistoryRead (#809). QualityMix
adds the reverse late-start variant (reverse TW pin + reverse oracle windows).

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-11-11 01:28:54 -05:00
parent 5bbe1e3c7a
commit e7b485c0a2
18 changed files with 328 additions and 59 deletions
@@ -359,7 +359,7 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
/// <inheritdoc />
public bool SupportsReverseEvents => true;
// <remarks>The seventeen <see cref="AggregateFamilies.Served"/> — shared with the other SQL backend.</remarks>
// <remarks>The nineteen <see cref="AggregateFamilies.Served"/> — shared with the other SQL backend.</remarks>
/// <inheritdoc />
public IReadOnlySet<HistoryAggregateType> SupportedAggregates => AggregateFamilies.Served;
@@ -463,7 +463,7 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
// 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) or its latest sample lies in (reverse: strictly inside).
// PercentGood / PercentBad are duration-based (time-weighted family, below). Count counts Good samples (value-based status; 0/Good on an empty bucket inside the tag's
// DurationGood / DurationBad / PercentGood / PercentBad are duration-based (time-weighted family, below). 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>
@@ -475,7 +475,7 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
// <see cref="TimeWeightedAggregator"/>, which fills every interval (an empty one between bounds gets a value).
// </para>
// </remarks>
// <exception cref="HistoryAggregateNotSupportedException">The aggregate is not one of the seventeen served.</exception>
// <exception cref="HistoryAggregateNotSupportedException">The aggregate is not one of the nineteen served.</exception>
// <exception cref="ArgumentOutOfRangeException">The window holds more than <see cref="MaxProcessedBuckets"/> intervals.</exception>
/// <inheritdoc />
public async Task<HistoryReadResult> ReadProcessedAsync(