fix(historian): T10 review — tick-exact bucketing over the full DateTime range; AtTime outage keeps its Bad code
Important 1: bucket offsets are now 100 ns ticks as bigint
(DATEDIFF_BIG(microsecond)*10 plus the sub-microsecond tick digits), so a
1601→9999 window no longer overflows (SQL 535) and nothing scales the interval
to nanoseconds in C#. Interval wider than the window clamps to one bucket and
the ceiling division cannot overflow. Exactness to the tick is unchanged.
Important 2 (spec §4 ruling d6afb394): a Bad previous neighbour means the time
is inside an outage — null value with that neighbour's Bad code, never
Uncertain. Usable previous + Bad next still holds the previous value Uncertain.
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+22
-9
@@ -33,7 +33,7 @@ namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer;
|
||||
/// or a driver's Bad publish) and is Bad whatever its code. Raw returns every sample as stored, outage
|
||||
/// markers included. Aggregates exclude Bad samples from the math, report the interval Uncertain when
|
||||
/// any non-Good sample is present, and count them in PercentBad. AtTime never interpolates across a Bad
|
||||
/// neighbour (see <see cref="ReadAtTimeAsync"/>).
|
||||
/// neighbour and reports a time inside an outage with the outage's Bad code (see <see cref="ReadAtTimeAsync"/>).
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public sealed class SqlServerHistorianDataSource : IHistorianDataSource
|
||||
@@ -41,6 +41,9 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
|
||||
/// <summary>Spec §9.7: a processed read covering more intervals than this is refused.</summary>
|
||||
public const int MaxProcessedBuckets = 100_000;
|
||||
|
||||
/// <summary>Severity + sub-code (top 16 bits) of a status, i.e. the code without info bits.</summary>
|
||||
private const uint StatusCodeBitsMask = 0xFFFF0000u;
|
||||
|
||||
private const string SampleColumns = "SourceTimeUtc, ValueKind, NumValue, IntValue, StrValue, StatusCode, ServerTimeUtc";
|
||||
|
||||
private readonly SqlHistorianConnection _conn;
|
||||
@@ -136,9 +139,11 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
|
||||
/// <list type="bullet">
|
||||
/// <item><c>p</c> at exactly <c>T</c> ⇒ the stored value and status, unchanged.</item>
|
||||
/// <item>No <c>p</c> ⇒ <c>BadNoData</c>, null value.</item>
|
||||
/// <item><c>p</c> or <c>n</c> Bad (incl. an <c>Empty</c> outage marker) ⇒ <c>UncertainDataSubNormal</c> +
|
||||
/// Interpolated bits, and NO interpolation across it: the value is <c>p</c>'s held value when <c>p</c> is
|
||||
/// usable (data held up to the outage), else null (inside an outage).</item>
|
||||
/// <item><c>p</c> Bad (incl. an <c>Empty</c> outage marker) ⇒ <c>T</c> lies inside an outage: null value
|
||||
/// carrying <c>p</c>'s Bad status code (info bits stripped) — never better than its source, per the
|
||||
/// quality contract (spec §4 as amended after the T10 review).</item>
|
||||
/// <item><c>p</c> usable and <c>n</c> Bad ⇒ <c>p</c>'s value held (stepped, NOT interpolated into the outage)
|
||||
/// with <c>UncertainDataSubNormal</c> + Interpolated bits.</item>
|
||||
/// <item>Double/Int64 with a usable <c>n</c> ⇒ linear interpolation of <c>NumValue</c> (Int64 rounded to the
|
||||
/// nearest integer, ties away from zero, keeping the type); status <c>Good</c> + Interpolated when both
|
||||
/// neighbours are Good, else <c>UncertainDataSubNormal</c> + Interpolated (never better than the
|
||||
@@ -208,8 +213,13 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
|
||||
if (p.TimeUtc == t)
|
||||
return new DataValueSnapshot(p.Value.ToObject(), p.Status, t, serverTime);
|
||||
|
||||
if (!p.IsUsable || next is { IsUsable: false })
|
||||
return new DataValueSnapshot(p.IsUsable ? p.Value.ToObject() : null,
|
||||
// Inside an outage: report the outage's own Bad code, never an upgraded Uncertain.
|
||||
if (!p.IsUsable)
|
||||
return new DataValueSnapshot(null, p.Status & StatusCodeBitsMask, t, serverTime);
|
||||
|
||||
// Usable previous, Bad next: hold the previous value up to the outage; do not interpolate into it.
|
||||
if (next is { IsUsable: false })
|
||||
return new DataValueSnapshot(p.Value.ToObject(),
|
||||
HistorianInfoBits.WithInterpolated(SqlHistorianStatusCodes.UncertainDataSubNormal), t, serverTime);
|
||||
|
||||
if (next is { } n && p.Value.Kind is HistorianValueKind.Double or HistorianValueKind.Int64
|
||||
@@ -328,8 +338,11 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
|
||||
var reverse = endUtc < startUtc;
|
||||
var (start, end) = reverse ? (endUtc, startUtc) : (startUtc, endUtc);
|
||||
var windowTicks = end.Ticks - start.Ticks;
|
||||
var intervalTicks = interval > TimeSpan.Zero ? interval.Ticks : windowTicks;
|
||||
long bucketCount = windowTicks == 0 ? 0 : (windowTicks + intervalTicks - 1) / intervalTicks;
|
||||
// Everything stays in 100 ns ticks (≤ ≈3.2e18 across 0001–9999, inside a long) — no ns scaling that could
|
||||
// wrap. An interval wider than the window is one bucket, so clamp it to the window; the ceiling division
|
||||
// is written without "+ interval - 1" so a TimeSpan near MaxValue cannot overflow either.
|
||||
var intervalTicks = interval > TimeSpan.Zero ? Math.Min(interval.Ticks, windowTicks) : windowTicks;
|
||||
long bucketCount = windowTicks == 0 ? 0 : windowTicks / intervalTicks + (windowTicks % intervalTicks == 0 ? 0 : 1);
|
||||
if (bucketCount > MaxProcessedBuckets)
|
||||
throw new ArgumentOutOfRangeException(nameof(interval), bucketCount,
|
||||
$"A processed read may cover at most {MaxProcessedBuckets} intervals; this window holds {bucketCount}.");
|
||||
@@ -353,7 +366,7 @@ public sealed class SqlServerHistorianDataSource : IHistorianDataSource
|
||||
cmd.Parameters.Add(new SqlParameter("@id", SqlDbType.Int) { Value = id });
|
||||
SqlHistorianConnection.AddDateTime2(cmd, "@start", DateTime.SpecifyKind(start, DateTimeKind.Utc));
|
||||
SqlHistorianConnection.AddDateTime2(cmd, "@end", DateTime.SpecifyKind(end, DateTimeKind.Utc));
|
||||
cmd.Parameters.Add(new SqlParameter("@intervalNs", SqlDbType.BigInt) { Value = intervalTicks * 100L });
|
||||
cmd.Parameters.Add(new SqlParameter("@intervalTicks", SqlDbType.BigInt) { Value = intervalTicks });
|
||||
await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false);
|
||||
while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user