fix(historian): #684 review — Uncertain non-numeric samples do not refuse the Good-value aggregates; real-SQL Uncertain-above-max test

- Average/Minimum/Maximum/Range/*ActualTime refuse (BadAggregateInvalidInputs) only a GOOD
  String/DateTime sample (new GoodNonNumericCount / good_non_numeric_count column); an Uncertain
  one is skipped and makes the min/max family Uncertain, matching the SDK (Average skips it,
  ComputeMinMax flags the uncastable value). Delta keeps its usable-sample refusal.
- ProcessedConformance: an Uncertain value above the Good maximum (pins UncMax/unc_max through
  the real SQL) and an Uncertain String through the real SQL.
- The_uncertain_dataset_is_not_vacuous also proves the min/max STATUS discrimination.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-10-12 09:54:51 -04:00
parent a11ab0dc08
commit c0d6bacd9c
13 changed files with 240 additions and 27 deletions
@@ -105,7 +105,8 @@ public sealed class BucketRowMappingTests
t.Columns.Add("SourceTimeUtc", typeof(DateTime));
t.Columns.Add("UncMin", typeof(double));
t.Columns.Add("UncMax", typeof(double));
t.Rows.Add(0L, 1, 1, 1, 0, 0, DBNull.Value, DBNull.Value, 4d, DBNull.Value);
t.Columns.Add("goodnonnum", typeof(int));
t.Rows.Add(0L, 1, 1, 1, 0, 0, DBNull.Value, DBNull.Value, 4d, DBNull.Value, 0);
var b = Map(t, AggregateFamily.ActualTime);
b.Extreme.ShouldBeNull();
@@ -121,7 +122,8 @@ public sealed class BucketRowMappingTests
t.Columns.Add("b", typeof(long));
foreach (var c in new[] { "total", "usable", "good", "bad", "nonnum" }) t.Columns.Add(c, typeof(int));
foreach (var c in new[] { "avg", "min", "max", "sum", "uncmin", "uncmax" }) t.Columns.Add(c, typeof(double));
t.Rows.Add(0L, 3, 3, 2, 0, 0, 20d, 10d, 30d, 40d, 5d, 25d);
t.Columns.Add("goodnonnum", typeof(int));
t.Rows.Add(0L, 4, 4, 2, 0, 1, 20d, 10d, 30d, 40d, 5d, 25d, 0);
var b = Map(t, AggregateFamily.Simple);
b.Avg.ShouldBe(20d);
@@ -130,9 +132,13 @@ public sealed class BucketRowMappingTests
b.Sum.ShouldBe(40d);
b.UncertainMin.ShouldBe(5d);
b.UncertainMax.ShouldBe(25d);
// Uncertain 5 is below the Good minimum ⇒ Minimum Uncertain; 25 is inside the Good range ⇒ Maximum Good.
b.NonNumeric.ShouldBe(1);
b.GoodNonNumeric.ShouldBe(0);
// Uncertain 5 is below the Good minimum ⇒ Minimum Uncertain. The one non-numeric sample is Uncertain (ordinal 12 says
// no Good one), so Average is served — not refused — and Maximum is Uncertain for it (25 alone is inside the range).
AggregateEvaluator.Evaluate(HistoryAggregateType.Minimum, b, T, T).StatusCode.ShouldBe(0x40A40401u);
AggregateEvaluator.Evaluate(HistoryAggregateType.Maximum, b, T, T).StatusCode.ShouldBe(0x00000401u);
AggregateEvaluator.Evaluate(HistoryAggregateType.Average, b, T, T).Value.ShouldBe(20d);
AggregateEvaluator.Evaluate(HistoryAggregateType.Maximum, b, T, T).StatusCode.ShouldBe(0x40A40401u);
}
[Fact]
@@ -145,7 +151,8 @@ public sealed class BucketRowMappingTests
t.Columns.Add("SourceTimeUtc", typeof(DateTime));
t.Columns.Add("UncMin", typeof(double));
t.Columns.Add("UncMax", typeof(double));
t.Rows.Add(0L, 3, 3, 2, 0, 0, 10d, T, 5d, 25d);
t.Columns.Add("goodnonnum", typeof(int));
t.Rows.Add(0L, 3, 3, 2, 0, 0, 10d, T, 5d, 25d, 0);
var b = Map(t, AggregateFamily.ActualTime);
b.Extreme.ShouldBe(10d);