fix(historian): #690 Gateway raw/AtTime Bad rows carry no value (a stored NULL was value 0)

This commit is contained in:
Joseph Doherty
2026-10-12 16:08:54 -04:00
parent 1f5d310071
commit 39552e15b9
6 changed files with 68 additions and 4 deletions
@@ -111,6 +111,21 @@ public sealed class GatewayHistorianDataSourceTests
Assert.Equal(0x80000000u, r.Samples[1].StatusCode); // gap → Bad at requested ts
}
[Fact] // #690: an at-time NULL (histsdk: value 0 + Bad) is null + Bad, like the SQL backends' AtTime of a Bad sample
public async Task ReadAtTime_of_a_NULL_row_carries_no_value()
{
var fake = new FakeHistorianGatewayClient();
var t0 = new DateTime(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
fake.AtTimeSamples = new[]
{
new HistorianSample { NumericValue = 0, OpcQuality = 0, Quality = 1, Timestamp = Timestamp.FromDateTime(t0) },
};
var ds = new GatewayHistorianDataSource(fake, NullLogger<GatewayHistorianDataSource>.Instance);
var r = await ds.ReadAtTimeAsync("T", new[] { t0 }, TestContext.Current.CancellationToken);
Assert.Null(r.Samples[0].Value);
Assert.Equal(0x80000000u, r.Samples[0].StatusCode);
}
[Fact]
public async Task Empty_window_is_not_a_fault()
{
@@ -109,6 +109,15 @@ public sealed class GatewayRawBoundaryRowTests
r.Samples[0].StatusCode.ShouldBe(0x80000000u);
}
[Fact] // #690: the wire's value 0 on a NULL row is not a value — SqlServer/QuestDB return null + Bad for the same sample
public async Task A_stored_NULL_sample_carries_no_value_not_the_wires_zero()
{
var r = await Source(Gate()).ReadRawAsync("T", At(30), At(55), 0, Ct);
r.Samples[0].SourceTimestampUtc.ShouldBe(At(40));
r.Samples[0].Value.ShouldBeNull();
r.Samples[1].Value.ShouldBe(5d); // a Good neighbour keeps its value
}
[Fact]
public async Task A_stored_sample_exactly_at_the_window_start_is_kept()
{
@@ -32,6 +32,31 @@ public sealed class SampleMapperTests
Assert.Equal(0x80000000u, SampleMapper.ToSnapshot(s).StatusCode);
}
[Theory] // #690: histsdk puts a value on every row, so a NULL row arrives as 0 / "" with a Bad quality
[InlineData(0u, 1u)] // the live NULL row: opc_quality 0, AVEVA quality 1
[InlineData(24u, 0u)] // any Bad OPC quality (out of service)
public void A_Bad_numeric_row_carries_no_value(uint opc, uint quality)
{
var s = new HistorianSample { Tag = "T", NumericValue = 0, OpcQuality = opc, Quality = quality, Timestamp = Ts(2026, 1, 1, 0, 0, 0) };
var snap = SampleMapper.ToSnapshot(s);
Assert.True(snap.StatusCode >= 0x80000000u);
Assert.Null(snap.Value);
}
[Fact] // #690: a string tag's NULL row arrives as "" + Bad
public void A_Bad_string_row_carries_no_value()
{
var s = new HistorianSample { Tag = "T", StringValue = "", OpcQuality = 0, Quality = 1, Timestamp = Ts(2026, 1, 1, 0, 0, 0) };
Assert.Null(SampleMapper.ToSnapshot(s).Value);
}
[Fact] // #690: an Uncertain row keeps its value (only Bad means "no value")
public void An_Uncertain_row_keeps_its_value()
{
var s = new HistorianSample { Tag = "T", NumericValue = 3, OpcQuality = 64, Timestamp = Ts(2026, 1, 1, 0, 0, 0) };
Assert.Equal(3d, SampleMapper.ToSnapshot(s).Value);
}
[Fact]
public void Aggregate_null_value_is_BadNoData()
{