using Microsoft.Data.SqlClient; using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common; namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests; /// /// Seeds a fresh schema with plain INSERTs (independent of the value writer) and builds a data source over it. /// Shared by the SQL Server-specific tests and by . is deliberately NOT millisecond-aligned. /// internal sealed class ReadTestDb { /// Base time: 2026-10-09 08:00:00.1234567 UTC (sub-millisecond ticks on purpose). public static readonly DateTime T0 = Conformance.ConformanceData.T0; public const uint Good = Conformance.ConformanceData.Good; public const uint Bad = Conformance.ConformanceData.Bad; public const uint BadNoCommunication = Conformance.ConformanceData.BadNoCommunication; public const uint Uncertain = Conformance.ConformanceData.Uncertain; private ReadTestDb(SqlServerHistorianOptions options, SqlHistorianConnection conn, SqlServerSchemaBootstrapper boot) { Options = options; Conn = conn; Boot = boot; } public SqlServerHistorianOptions Options { get; } public SqlHistorianConnection Conn { get; } public SqlServerSchemaBootstrapper Boot { get; } public static DateTime At(double seconds) => Conformance.ConformanceData.At(seconds); public static async Task CreateAsync(SqlHistorianFixture fx) { fx.SkipIfUnavailable(); var o = new SqlServerHistorianOptions { ConnectionString = fx.ConnectionString, Schema = SqlHistorianFixture.UniqueSchema() }; var conn = new SqlHistorianConnection(o); var boot = new SqlServerSchemaBootstrapper(conn, o); await boot.EnsureAsync(CancellationToken.None); return new ReadTestDb(o, conn, boot); } /// A fresh data source (fresh catalog cache) over this schema; is its "now". public SqlServerHistorianDataSource Source(TimeProvider? clock = null, int maxTimeWeightedRawRows = TimeWeightedAggregator.DefaultMaxWindowRows) => new(Conn, Boot, new SqlServerTagCatalog(Conn, Boot), timeProvider: clock, maxTimeWeightedRawRows: maxTimeWeightedRawRows); /// Inserts a tag row and the given samples. ServerTimeUtc = source time + 1 s (so tests can tell them apart). public async Task SeedAsync(string tag, params (DateTime T, HistorianValue V, uint Status)[] samples) { await using var c = await Conn.OpenAsync(CancellationToken.None); int id; await using (var cmd = Conn.CreateCommand(c, $"INSERT {Conn.Q("Tag")} (Name, DataType, CreatedUtc, UpdatedUtc) OUTPUT inserted.TagId VALUES (@n, 0, SYSUTCDATETIME(), SYSUTCDATETIME())")) { cmd.Parameters.AddWithValue("@n", tag); id = (int)(await cmd.ExecuteScalarAsync())!; } foreach (var (t, v, status) in samples) { var col = SampleColumnMapper.ToColumns(v); await using var cmd = Conn.CreateCommand(c, $"INSERT {Conn.Q("Sample")} (TagId, SourceTimeUtc, ValueKind, NumValue, IntValue, StrValue, StatusCode, ServerTimeUtc) " + "VALUES (@id, @t, @k, @num, @int, @str, @st, @srv)"); cmd.Parameters.AddWithValue("@id", id); SqlHistorianConnection.AddDateTime2(cmd, "@t", t); cmd.Parameters.AddWithValue("@k", col.ValueKind); cmd.Parameters.Add(new SqlParameter("@num", System.Data.SqlDbType.Float) { Value = (object?)col.NumValue ?? DBNull.Value }); cmd.Parameters.Add(new SqlParameter("@int", System.Data.SqlDbType.BigInt) { Value = (object?)col.IntValue ?? DBNull.Value }); cmd.Parameters.Add(new SqlParameter("@str", System.Data.SqlDbType.NVarChar, -1) { Value = (object?)col.StrValue ?? DBNull.Value }); cmd.Parameters.AddWithValue("@st", SampleColumnMapper.ToDbStatus(status)); SqlHistorianConnection.AddDateTime2(cmd, "@srv", t.AddSeconds(1)); await cmd.ExecuteNonQueryAsync(); } return id; } public async Task ScalarAsync(string sql) { await using var c = await Conn.OpenAsync(CancellationToken.None); await using var cmd = Conn.CreateCommand(c, sql); return Convert.ToInt64(await cmd.ExecuteScalarAsync()); } }