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:
Joseph Doherty
2026-10-09 13:30:54 -04:00
parent 38d37f5c23
commit 8f859acb2c
4 changed files with 91 additions and 22 deletions
@@ -4,7 +4,11 @@ using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests;
/// <summary>AtTime rules (spec §4): exact, linear, stepped, no-prior, Bad neighbour (no bridging).</summary>
/// <summary>
/// AtTime rules (spec §4, as amended after the T10 review): exact, linear, stepped, no-prior, and the two
/// Bad-neighbour cases — inside an outage (Bad previous) ⇒ null with the outage's own Bad code; before an
/// outage (usable previous, Bad next) ⇒ previous value held, Uncertain. Never bridges.
/// </summary>
[Collection(SqlHistorianCollection.Name)]
public sealed class AtTimeReadTests(SqlHistorianFixture fx)
{
@@ -75,13 +79,14 @@ public sealed class AtTimeReadTests(SqlHistorianFixture fx)
}
[Fact]
public async Task A_bad_previous_neighbour_yields_no_value_uncertain()
public async Task A_bad_previous_neighbour_is_inside_an_outage_and_keeps_its_bad_code()
{
var db = await ReadTestDb.CreateAsync(fx);
await db.SeedAAsync();
var s = (await db.Source().ReadAtTimeAsync("A", [At(25)], Ct)).Samples.Single();
s.Value.ShouldBeNull(); // NOT 35 (30→40) and NOT 30 (a Bad value)
s.StatusCode.ShouldBe(UncertainInterpolated);
s.Value.ShouldBeNull(); // NOT 35 (30→40) and NOT 30 (a Bad value)
s.StatusCode.ShouldBe(0x80000000u); // the previous sample's Bad code — never upgraded to Uncertain
s.SourceTimestampUtc.ShouldBe(At(25));
}
[Fact]
@@ -96,7 +101,7 @@ public sealed class AtTimeReadTests(SqlHistorianFixture fx)
s[0].Value.ShouldBe(10d); // bridging 10→30 would give 15
s[0].StatusCode.ShouldBe(UncertainInterpolated);
s[1].Value.ShouldBeNull(); // bridging would give 25
s[1].StatusCode.ShouldBe(UncertainInterpolated);
s[1].StatusCode.ShouldBe(0x80310000u); // inside the outage: BadNoCommunication, as the marker says
s[2].Value.ShouldBeNull(); // exact hit on the marker: as stored
s[2].StatusCode.ShouldBe(0x80310000u);
}