fix(historian): #681 on #682/#684 — Gateway served set is the 12-member ServedAggregates; raw buckets use Good-only values

- #682: ServedAggregates (ten raw-evaluated per-sample + TimeAverage/Total) replaces the placeholder
  AggregateModeMapper.Served; NativeAggregates is derived from the mapper's switch. The Gateway
  expectations in AggregateModeMapperTests, HistorianBackendRegistryTests and
  AdvertisedAggregateFunctionsWireTests move to the 12-member set (the wire tests' 'not served by
  Gateway' example is now TimeAverage2).
- #684: RawBucketAccumulator applies the SQL backends' Good-only value columns (Avg/Min/Max/Sum and
  the ActualTime extreme over Good numeric samples), UncertainMin/UncertainMax over Uncertain numeric
  samples, and GoodNonNumeric. The parity oracle carries the #684 InMemoryRows and the DenseUncertain /
  UncertainBeyond / UncertainText datasets.
This commit is contained in:
Joseph Doherty
2026-10-12 10:00:30 -04:00
parent 78f604e0cd
commit 5f225f0b44
4 changed files with 66 additions and 20 deletions
@@ -211,8 +211,10 @@ internal static class RawAggregateReader
/// <summary>
/// Builds the per-bucket statistics (<see cref="BucketRow"/>) the SQL backends' aggregate queries produce, from a stream
/// of raw samples — the C# statement of <c>SqlAggregateQueries</c>' columns: counts by class (usable = non-Bad with a
/// value; good = usable with Good severity), Avg/Min/Max/Sum over the usable numeric values, first/last usable value by
/// time, the extreme usable numeric value with its time (ties ⇒ earliest), and the first/last sample of any quality.
/// value; good = usable with Good severity; NonNumeric = usable String/DateTime; GoodNonNumeric = good String/DateTime),
/// Avg/Min/Max/Sum over the GOOD numeric values and UncertainMin/UncertainMax over the usable-but-not-Good numeric values
/// (#684), first/last usable value by time, the extreme GOOD numeric value with its time (ties ⇒ earliest), and the
/// first/last sample of any quality.
/// </summary>
/// <remarks>
/// Keep in step with the SQL backends' per-sample value columns: a change to which samples feed a statistic there
@@ -229,8 +231,9 @@ internal sealed class RawBucketAccumulator
private sealed class State
{
public int Total, Usable, Good, NonNumeric, Numeric;
public int Total, Usable, Good, NonNumeric, GoodNonNumeric, GoodNumeric, UncertainNumeric;
public double Sum, Min = double.PositiveInfinity, Max = double.NegativeInfinity;
public double UncertainMin = double.PositiveInfinity, UncertainMax = double.NegativeInfinity;
public StoredSample? First, Last, FirstAny, LastAny, Extreme;
}
@@ -251,9 +254,18 @@ internal sealed class RawBucketAccumulator
if (!s.Value.TryGetDouble(out var v))
{
st.NonNumeric++;
if (s.IsGood) st.GoodNonNumeric++;
return;
}
st.Numeric++;
if (!s.IsGood)
{
// #684: an Uncertain value is not part of the value set; it only feeds the Min/Max status rule.
st.UncertainNumeric++;
if (v < st.UncertainMin) st.UncertainMin = v;
if (v > st.UncertainMax) st.UncertainMax = v;
return;
}
st.GoodNumeric++;
st.Sum += v;
if (v < st.Min) st.Min = v;
if (v > st.Max) st.Max = v;
@@ -272,7 +284,8 @@ internal sealed class RawBucketAccumulator
_buckets.OrderBy(kv => kv.Key).Select(kv =>
{
var st = kv.Value;
var hasNumeric = st.Numeric > 0;
var hasNumeric = st.GoodNumeric > 0;
var hasUncertain = st.UncertainNumeric > 0;
return new BucketRow
{
Bucket = kv.Key,
@@ -281,10 +294,13 @@ internal sealed class RawBucketAccumulator
Good = st.Good,
Bad = st.Total - st.Usable,
NonNumeric = st.NonNumeric,
Avg = hasNumeric ? st.Sum / st.Numeric : null,
GoodNonNumeric = st.GoodNonNumeric,
Avg = hasNumeric ? st.Sum / st.GoodNumeric : null,
Min = hasNumeric ? st.Min : null,
Max = hasNumeric ? st.Max : null,
Sum = hasNumeric ? st.Sum : null,
UncertainMin = hasUncertain ? st.UncertainMin : null,
UncertainMax = hasUncertain ? st.UncertainMax : null,
First = st.First?.Value ?? HistorianValue.Empty,
Last = st.Last?.Value ?? HistorianValue.Empty,
Extreme = st.Extreme?.Value.Number,