c5b33aa123
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
88 lines
4.3 KiB
C#
88 lines
4.3 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// 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 <see cref="SqlServerConformanceBackend"/>. <see cref="T0"/> is deliberately NOT millisecond-aligned.
|
|
/// </summary>
|
|
internal sealed class ReadTestDb
|
|
{
|
|
/// <summary>Base time: 2026-10-09 08:00:00.1234567 UTC (sub-millisecond ticks on purpose).</summary>
|
|
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<ReadTestDb> 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);
|
|
}
|
|
|
|
/// <summary>A fresh data source (fresh catalog cache) over this schema; <paramref name="clock"/> is its "now".</summary>
|
|
public SqlServerHistorianDataSource Source(TimeProvider? clock = null) =>
|
|
new(Conn, Boot, new SqlServerTagCatalog(Conn, Boot), timeProvider: clock);
|
|
|
|
/// <summary>Inserts a tag row and the given samples. ServerTimeUtc = source time + 1 s (so tests can tell them apart).</summary>
|
|
public async Task<int> 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<long> 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());
|
|
}
|
|
}
|
|
|