feat(historian): Gateway serves the per-sample aggregates from raw reads with the shared Sql.Common evaluator (#681)

Average, Count, Minimum, Maximum, Range, Start, End, Delta and Minimum/MaximumActualTime are evaluated
on the Gateway backend from forward ReadRaw reads with BucketPlan + AggregateEvaluator, so Gateway
matches SqlServer/QuestDB exactly (values, status bits, interval-start stamps, #677 reverse).

- Window read [start - 1 ms, end] so AVEVA's initial-value row never lands in a bucket; a pre-window
  row proves earlier data only with a non-Bad OPC quality (histsdk puts a value on every NULL row).
- Earlier data otherwise from a 2-row [1970, start) probe; later data from a 2-row [end, now+1d]
  look-ahead; both ignore rows stamped at their query start/end.
- No gateway stream is ever abandoned (a client-side cancel makes the gateway evict its shared
  historian session); past ServerHistorian:MaxTimeWeightedRawRows the bounded stream is drained, then
  BadTooManyOperations. The gateway's max_values-ceiling InvalidArgument maps to the same limit; a
  caller cancellation is not a backend fault.
- TimeAverage/Total stay AVEVA-native (no prior-sample read for exact Part 13 bounds), now stamped with
  the interval start (PENDING the live leg: histsdk's tail+43 start_time field is unverified) and
  carrying the Calculated bits.
- Served set: GatewayHistorianDataSource.ServedAggregates (12). Live AVEVA leg open (VPN).
This commit is contained in:
Joseph Doherty
2026-10-12 09:55:32 -04:00
parent dbdf29c8fc
commit 78f604e0cd
16 changed files with 1187 additions and 77 deletions
@@ -149,8 +149,14 @@ public sealed class HistorianBackendRegistryTests
IHistorianDataSource Source(ServerHistorianOptions o, IConfiguration c) =>
HistorianBackendRegistry.Resolve(o, c).CreateDataSource(sp);
// #681: the ten per-sample aggregates (raw-evaluated) + the two AVEVA-native ones.
Source(Gateway(), Cfg).SupportedAggregates.ShouldBe(
[HistoryAggregateType.TimeAverage, HistoryAggregateType.Minimum, HistoryAggregateType.Maximum, HistoryAggregateType.Total],
[
HistoryAggregateType.Average, HistoryAggregateType.Count, HistoryAggregateType.Minimum,
HistoryAggregateType.Maximum, HistoryAggregateType.Range, HistoryAggregateType.Start, HistoryAggregateType.End,
HistoryAggregateType.Delta, HistoryAggregateType.MinimumActualTime, HistoryAggregateType.MaximumActualTime,
HistoryAggregateType.TimeAverage, HistoryAggregateType.Total,
],
ignoreOrder: true);
Source(new ServerHistorianOptions { Backend = "SqlServer", Enabled = true }, SqlCfg())
.SupportedAggregates.ShouldBe(SqlServed, ignoreOrder: true);
@@ -20,7 +20,12 @@ namespace ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests;
/// </summary>
public sealed class AdvertisedAggregateFunctionsWireTests
{
private static readonly string[] GatewayNames = ["Maximum", "Minimum", "TimeAverage", "Total"];
// #681: the ten per-sample aggregates (raw-evaluated) + TimeAverage / Total (AVEVA-native).
private static readonly string[] GatewayNames =
[
"Average", "Count", "Delta", "End", "Maximum", "MaximumActualTime", "Minimum", "MinimumActualTime", "Range", "Start",
"TimeAverage", "Total",
];
private static readonly string[] SqlNames =
[