fix(historian): #680 lane B — per-sample Count/Start/End/Delta per Part 13 + forward end-of-data Partial
Count: Good samples only, value-based status (all Bad => generic Bad), 0/Good on an empty interval inside the tag's data range [first, last] (both directions). Start/End: the raw first/last sample of any quality (own value, status, timestamp). Delta: Uncertain only when the interval's first or last sample is not usable. Forward end-of-data Partial (SDK m_endOfData, earlier edge inclusive) for every per-sample aggregate but Average (PercentGood/Bad left to lane A). Backends push down first/last-any-quality columns and fetch the tag's data range (earliest <= window end, latest >= window start) only when NeedsDataRange. SDK parity masks for Start/End/Delta/Count removed; new parity datasets.
This commit is contained in:
+19
-11
@@ -410,8 +410,11 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
|
||||
/// 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). Every aggregate but Average sets the Partial
|
||||
/// bit on the bucket the tag's earliest sample lies strictly inside (#677).
|
||||
/// 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
|
||||
/// 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>
|
||||
/// <para>
|
||||
/// Values: Count is <see cref="int"/>; Start/End keep the sample's type; every other aggregate is
|
||||
@@ -444,7 +447,7 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
|
||||
return new HistoryReadResult(twResult, ContinuationPoint: null);
|
||||
}
|
||||
var rows = new List<BucketRow>();
|
||||
DateTime? earliest = null;
|
||||
DateTime? earliest = null, latest = null;
|
||||
if (tagId is { } id)
|
||||
{
|
||||
var sample = _conn.Q("Sample");
|
||||
@@ -454,10 +457,10 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
|
||||
AggregateFamily.StartEnd => SqlAggregateQueries.StartEnd(sample, plan.Reversed),
|
||||
_ => SqlAggregateQueries.ActualTime(sample, aggregate == HistoryAggregateType.MaximumActualTime, plan.Reversed),
|
||||
};
|
||||
// Second result set: the tag's earliest sample (the start-of-data Partial rule, #677) — forward reads of an
|
||||
// aggregate other than Average only.
|
||||
var needsEarliest = AggregateEvaluator.NeedsEarliestSample(aggregate, plan);
|
||||
if (needsEarliest) sql += " " + SqlAggregateQueries.EarliestSample(sample);
|
||||
// Second result set: the tag's data range (earliest + latest sample) — the start/end-of-data Partial rules
|
||||
// (#677/#680, forward reads of an aggregate other than Average) and Count's in-range empty interval (#680).
|
||||
var needsRange = AggregateEvaluator.NeedsDataRange(aggregate, plan);
|
||||
if (needsRange) sql += " " + SqlAggregateQueries.DataRange(sample);
|
||||
await using var conn = await _conn.OpenAsync(cancellationToken).ConfigureAwait(false);
|
||||
await using var cmd = _conn.CreateCommand(conn, sql);
|
||||
cmd.Parameters.Add(new SqlParameter("@id", SqlDbType.Int) { Value = id });
|
||||
@@ -468,12 +471,15 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
|
||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
rows.Add(ReadBucket(reader, family));
|
||||
if (needsEarliest && await reader.NextResultAsync(cancellationToken).ConfigureAwait(false)
|
||||
&& await reader.ReadAsync(cancellationToken).ConfigureAwait(false) && !reader.IsDBNull(0))
|
||||
earliest = DateTime.SpecifyKind(reader.GetDateTime(0), DateTimeKind.Utc);
|
||||
if (needsRange && await reader.NextResultAsync(cancellationToken).ConfigureAwait(false)
|
||||
&& await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
if (!reader.IsDBNull(0)) earliest = SqlHistorianConnection.GetUtc(reader, 0);
|
||||
if (!reader.IsDBNull(1)) latest = SqlHistorianConnection.GetUtc(reader, 1);
|
||||
}
|
||||
}
|
||||
|
||||
var result = AggregateEvaluator.EvaluateAll(aggregate, plan, rows, earliest, now);
|
||||
var result = AggregateEvaluator.EvaluateAll(aggregate, plan, rows, earliest, latest, now);
|
||||
|
||||
RecordSuccess();
|
||||
return new HistoryReadResult(result, ContinuationPoint: null);
|
||||
@@ -547,6 +553,8 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
|
||||
case AggregateFamily.StartEnd:
|
||||
row.First = V(reader, f);
|
||||
row.Last = V(reader, f + 5);
|
||||
row.FirstAny = ReadStored(reader, SqlAggregateQueries.FirstAnyColumn);
|
||||
row.LastAny = ReadStored(reader, SqlAggregateQueries.FirstAnyColumn + 6);
|
||||
break;
|
||||
default:
|
||||
row.Extreme = D(reader, f);
|
||||
|
||||
Reference in New Issue
Block a user