38d37f5c23
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
93 lines
3.9 KiB
C#
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"]);
|
|
}
|
|
}
|