Files
lmxopcua/tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests/EventReadTests.cs
T
Joseph Doherty 38d37f5c23 feat(historian): SQL Server read side — raw, at-time, events, push-down processed (ph-T10)
SqlServerHistorianDataSource implements IHistorianDataSource over the T8 schema:
raw forward/reverse/point with the 0 = unbounded sentinel; AtTime in one statement
(TimeRow TVP + two OUTER APPLYs) with linear/stepped/no-prior rules and no
interpolation across a Bad neighbour or Empty outage marker; events with a
maxEvents+1 truncation probe and a non-null continuation point; processed
aggregates pushed down to T-SQL (SqlAggregateQueries) with nanosecond-exact
bucket edges, Calculated bits on every result, Uncertain on any non-Good input,
BadAggregateInvalidInputs for numeric aggregates over String/DateTime, a
100 000-bucket cap and HistoryAggregateNotSupportedException for the rest.

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
2026-10-09 13:13:07 -04:00

93 lines
3.9 KiB
C#

using System.Text;
using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests;
[Collection(SqlHistorianCollection.Name)]
public sealed class EventReadTests(SqlHistorianFixture fx)
{
private static readonly CancellationToken Ct = CancellationToken.None;
private static DateTime At(double s) => ReadTestDb.At(s);
private static async Task InsertEventAsync(ReadTestDb db, string id, string source, DateTime t, string? message, int severity)
{
await using var c = await db.Conn.OpenAsync(Ct);
await using var cmd = db.Conn.CreateCommand(c,
$"INSERT {db.Conn.Q("Event")} (EventId, SourceName, AlarmId, AlarmName, AlarmTypeName, Severity, EventKind, Message, " +
"UserName, Comment, EventTimeUtc, ReceivedTimeUtc) VALUES (@id, @src, 'a', 'n', 'T', @sev, 'Activated', @msg, NULL, NULL, @t, @r)");
cmd.Parameters.AddWithValue("@id", id);
cmd.Parameters.AddWithValue("@src", source);
cmd.Parameters.AddWithValue("@sev", severity);
cmd.Parameters.AddWithValue("@msg", (object?)message ?? DBNull.Value);
SqlHistorianConnection.AddDateTime2(cmd, "@t", t);
SqlHistorianConnection.AddDateTime2(cmd, "@r", t.AddTicks(3));
await cmd.ExecuteNonQueryAsync();
}
private static async Task<ReadTestDb> SeedAsync(SqlHistorianFixture fx)
{
var db = await ReadTestDb.CreateAsync(fx);
await InsertEventAsync(db, "e1", "Eq/1", At(0), "first", 500);
await InsertEventAsync(db, "e2", "Eq/2", At(1), null, 700);
await InsertEventAsync(db, "e3", "Eq/1", At(2), "third", 900);
await InsertEventAsync(db, "e4", "Eq/1", At(3), "at end", 100);
return db;
}
[Fact]
public async Task Source_filter_window_order_and_field_mapping()
{
var db = await SeedAsync(fx);
var r = await db.Source().ReadEventsAsync("Eq/1", At(0), At(3), 0, Ct);
r.Events.Select(e => e.EventId).ShouldBe(["e1", "e3"]); // e4 at end excluded, e2 other source
var e = r.Events[0];
e.SourceName.ShouldBe("Eq/1");
e.EventTimeUtc.ShouldBe(At(0));
e.EventTimeUtc.Kind.ShouldBe(DateTimeKind.Utc);
e.ReceivedTimeUtc.ShouldBe(At(0).AddTicks(3));
e.Message.ShouldBe("first");
e.Severity.ShouldBe((ushort)500);
r.ContinuationPoint.ShouldBeNull();
}
[Fact]
public async Task Null_source_returns_every_source_and_null_messages()
{
var db = await SeedAsync(fx);
var r = await db.Source().ReadEventsAsync(null, At(0), At(10), 0, Ct);
r.Events.Select(e => e.EventId).ShouldBe(["e1", "e2", "e3", "e4"]);
r.Events[1].Message.ShouldBeNull();
}
[Fact]
public async Task Truncation_drops_the_extra_row_and_returns_a_continuation_point()
{
var db = await SeedAsync(fx);
var r = await db.Source().ReadEventsAsync(null, At(0), At(3), 2, Ct); // 3 matching rows
r.Events.Select(e => e.EventId).ShouldBe(["e1", "e2"]);
r.ContinuationPoint.ShouldNotBeNull();
Encoding.UTF8.GetString(r.ContinuationPoint!).ShouldBe($"{At(2).Ticks}|e3");
}
[Fact]
public async Task An_exact_fit_or_an_unbounded_read_has_no_continuation_point()
{
var db = await SeedAsync(fx);
var src = db.Source();
var exact = await src.ReadEventsAsync(null, At(0), At(3), 3, Ct);
exact.Events.Count.ShouldBe(3);
exact.ContinuationPoint.ShouldBeNull();
(await src.ReadEventsAsync(null, At(0), At(10), int.MaxValue, Ct)).Events.Count.ShouldBe(4); // node manager's "no limit"
(await src.ReadEventsAsync(null, At(0), At(10), -1, Ct)).ContinuationPoint.ShouldBeNull();
}
[Fact]
public async Task A_reversed_window_is_swapped()
{
var db = await SeedAsync(fx);
(await db.Source().ReadEventsAsync(null, At(3), At(0), 0, Ct)).Events.Select(e => e.EventId)
.ShouldBe(["e1", "e2", "e3"]);
}
}