refactor(historian): extract Driver.Historian.Sql.Common from the SQL Server backend (no behaviour change) (1a)
Moves the database-independent logic of the SQL Server historian backend into a new shared library for the upcoming QuestDB backend (#666): status codes and bits, value/column mapping, StoredSample + quality clamp, the AtTime and aggregate evaluators, BucketPlan, LiveWritePreparer and HistoryUpdatePlanner (both with a resolutionTicks parameter, default 1), tag-name rules, the bootstrap failure cache, health counters, alarm event ids, the event continuation and severity clamp. The SQL Server backend now calls it; its T-SQL, TVP builders, connection, bootstrapper and catalog stay. Tests: StatusCodeOracleTests, ValueWriterPrepareTests, the column half of SampleRowMapperTests and ProcessedReadTests.Every_enum_member_is_either_served_or_refused move to the new Sql.Common.Tests under the same names. SqlServer integration tests get mechanical renames only. Test-name set across the three projects is identical to the baseline (160 tests). Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+2
@@ -43,6 +43,8 @@
|
||||
DriverTypeNames guard skips it — but it does hard-code OPC UA status constants, which
|
||||
puts it in StatusCodeParityTests' scope. -->
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway\ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.csproj"/>
|
||||
<!-- The SQL historian backends' shared status constants (HistorianStatusCodes) live here. -->
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common\ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.csproj"/>
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer\ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.Tests;
|
||||
|
||||
/// <summary>The DB-free part of the former SQL Server integration <c>ProcessedReadTests</c>.</summary>
|
||||
public sealed class ProcessedReadTests
|
||||
{
|
||||
[Fact]
|
||||
public void Every_enum_member_is_either_served_or_refused()
|
||||
{
|
||||
// Thirteen served; anything appended to the enum later falls to the refusal (total default).
|
||||
Enum.GetValues<HistoryAggregateType>().Count(a => AggregateFamilies.FamilyOf(a) is not null).ShouldBe(13);
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.Tests;
|
||||
|
||||
/// <summary>The column round-trip half of the former SQL Server <c>SampleRowMapperTests</c> (the TVP half stays there).</summary>
|
||||
public sealed class SampleRowMapperTests
|
||||
{
|
||||
public static TheoryData<HistorianValue> Values => new()
|
||||
{
|
||||
HistorianValue.FromDouble(12.5),
|
||||
HistorianValue.FromInt64(long.MinValue + 3),
|
||||
HistorianValue.FromBoolean(true),
|
||||
HistorianValue.FromBoolean(false),
|
||||
HistorianValue.FromString("héllo"),
|
||||
HistorianValue.FromString(""),
|
||||
HistorianValue.FromDateTime(new DateTime(2026, 10, 9, 1, 2, 3, DateTimeKind.Utc).AddTicks(1234567)),
|
||||
HistorianValue.Empty,
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Values))]
|
||||
public void Every_kind_round_trips_through_the_column_quad(HistorianValue v)
|
||||
{
|
||||
var c = SampleColumnMapper.ToColumns(v);
|
||||
SampleColumnMapper.FromColumns(c.ValueKind, c.NumValue, c.IntValue, c.StrValue).ShouldBe(v);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Columns_follow_the_spec_mapping()
|
||||
{
|
||||
SampleColumnMapper.ToColumns(HistorianValue.FromInt64(7)).ShouldBe(new SampleColumns(2, 7d, 7L, null));
|
||||
SampleColumnMapper.ToColumns(HistorianValue.FromBoolean(true)).ShouldBe(new SampleColumns(3, 1d, 1L, null));
|
||||
SampleColumnMapper.ToColumns(HistorianValue.FromDouble(1.5)).ShouldBe(new SampleColumns(1, 1.5, null, null));
|
||||
SampleColumnMapper.ToColumns(HistorianValue.Empty).ShouldBe(new SampleColumns(0, null, null, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Status_is_stored_bit_for_bit()
|
||||
{
|
||||
SampleColumnMapper.ToDbStatus(0x80050000u).ShouldBeLessThan(0);
|
||||
SampleColumnMapper.FromDbStatus(SampleColumnMapper.ToDbStatus(0x80050000u)).ShouldBe(0x80050000u);
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -1,7 +1,7 @@
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.Tests;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.Tests;
|
||||
|
||||
/// <summary>The pinned SDK is the oracle for the info-bit constants (StatusCodeParityTests covers the named codes).</summary>
|
||||
public sealed class StatusCodeOracleTests
|
||||
@@ -10,7 +10,7 @@ public sealed class StatusCodeOracleTests
|
||||
public void Calculated_bits_match_the_SDK()
|
||||
{
|
||||
var sdk = new Opc.Ua.StatusCode(Opc.Ua.StatusCodes.Good).SetAggregateBits(Opc.Ua.AggregateBits.Calculated);
|
||||
sdk.Code.ShouldBe(HistorianInfoBits.WithCalculated(SqlHistorianStatusCodes.Good));
|
||||
sdk.Code.ShouldBe(HistorianInfoBits.WithCalculated(HistorianStatusCodes.Good));
|
||||
((uint)Opc.Ua.AggregateBits.Calculated).ShouldBe(HistorianInfoBits.HistorianCalculated);
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ public sealed class StatusCodeOracleTests
|
||||
public void Interpolated_bits_match_the_SDK()
|
||||
{
|
||||
var sdk = new Opc.Ua.StatusCode(Opc.Ua.StatusCodes.Good).SetAggregateBits(Opc.Ua.AggregateBits.Interpolated);
|
||||
sdk.Code.ShouldBe(HistorianInfoBits.WithInterpolated(SqlHistorianStatusCodes.Good));
|
||||
sdk.Code.ShouldBe(HistorianInfoBits.WithInterpolated(HistorianStatusCodes.Good));
|
||||
((uint)Opc.Ua.AggregateBits.Interpolated).ShouldBe(HistorianInfoBits.HistorianInterpolated);
|
||||
}
|
||||
|
||||
@@ -30,9 +30,9 @@ public sealed class StatusCodeOracleTests
|
||||
[Fact]
|
||||
public void Setting_one_historian_bit_clears_the_other()
|
||||
{
|
||||
var calculatedUncertain = SqlHistorianStatusCodes.UncertainDataSubNormal | 0x400u | HistorianInfoBits.HistorianCalculated;
|
||||
var calculatedUncertain = HistorianStatusCodes.UncertainDataSubNormal | 0x400u | HistorianInfoBits.HistorianCalculated;
|
||||
var interpolated = HistorianInfoBits.WithInterpolated(calculatedUncertain);
|
||||
interpolated.ShouldBe(SqlHistorianStatusCodes.UncertainDataSubNormal | 0x400u | HistorianInfoBits.HistorianInterpolated);
|
||||
interpolated.ShouldBe(HistorianStatusCodes.UncertainDataSubNormal | 0x400u | HistorianInfoBits.HistorianInterpolated);
|
||||
new Opc.Ua.StatusCode(interpolated).AggregateBits.ShouldBe(Opc.Ua.AggregateBits.Interpolated);
|
||||
|
||||
var interpolatedGood = 0x400u | HistorianInfoBits.HistorianInterpolated;
|
||||
+9
-9
@@ -2,7 +2,7 @@ using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.Tests;
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.Tests;
|
||||
|
||||
public sealed class ValueWriterPrepareTests
|
||||
{
|
||||
@@ -13,7 +13,7 @@ public sealed class ValueWriterPrepareTests
|
||||
{
|
||||
const uint uncertain = 0x40000000u;
|
||||
const uint badNoCommunication = 0x80310000u;
|
||||
var rows = SqlServerHistorianValueWriter.Prepare(
|
||||
var rows = LiveWritePreparer.Prepare(
|
||||
[
|
||||
new(T, HistorianValue.FromDouble(double.NaN), uncertain),
|
||||
new(T.AddTicks(1), HistorianValue.FromDouble(double.PositiveInfinity), 0),
|
||||
@@ -21,26 +21,26 @@ public sealed class ValueWriterPrepareTests
|
||||
new(T.AddTicks(3), HistorianValue.FromDouble(1.5), 0),
|
||||
], () => T);
|
||||
rows.Select(r => r.Value.Kind).ShouldBe([HistorianValueKind.Empty, HistorianValueKind.Empty, HistorianValueKind.Empty, HistorianValueKind.Double]);
|
||||
rows.Select(r => r.StatusCode).ShouldBe([SqlHistorianStatusCodes.BadOutOfRange, SqlHistorianStatusCodes.BadOutOfRange,
|
||||
rows.Select(r => r.StatusCode).ShouldBe([HistorianStatusCodes.BadOutOfRange, HistorianStatusCodes.BadOutOfRange,
|
||||
badNoCommunication, 0u]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void A_non_bad_Empty_is_stored_BadNoData_and_a_bad_Empty_keeps_its_code()
|
||||
{
|
||||
var rows = SqlServerHistorianValueWriter.Prepare(
|
||||
var rows = LiveWritePreparer.Prepare(
|
||||
[
|
||||
new(T, HistorianValue.Empty, 0),
|
||||
new(T.AddTicks(1), HistorianValue.Empty, 0x40A40000u),
|
||||
new(T.AddTicks(2), HistorianValue.Empty, 0x80310000u),
|
||||
], () => T);
|
||||
rows.Select(r => r.StatusCode).ShouldBe([SqlHistorianStatusCodes.BadNoData, SqlHistorianStatusCodes.BadNoData, 0x80310000u]);
|
||||
rows.Select(r => r.StatusCode).ShouldBe([HistorianStatusCodes.BadNoData, HistorianStatusCodes.BadNoData, 0x80310000u]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Duplicate_timestamps_keep_the_first_and_null_timestamp_uses_the_clock()
|
||||
{
|
||||
var rows = SqlServerHistorianValueWriter.Prepare(
|
||||
var rows = LiveWritePreparer.Prepare(
|
||||
[
|
||||
new(T, HistorianValue.FromInt64(1), 0), new(T, HistorianValue.FromInt64(2), 0),
|
||||
new(null, HistorianValue.FromInt64(3), 0),
|
||||
@@ -53,8 +53,8 @@ public sealed class ValueWriterPrepareTests
|
||||
[Fact]
|
||||
public void Data_type_mapping_is_one_table()
|
||||
{
|
||||
SqlHistorianDataTypes.FromKind(HistorianValueKind.Double).ShouldBe((byte)DriverDataType.Float64);
|
||||
SqlHistorianDataTypes.FromKind(HistorianValueKind.Boolean).ShouldBe((byte)DriverDataType.Boolean);
|
||||
SqlHistorianDataTypes.FromKind(HistorianValueKind.Empty).ShouldBe((byte)DriverDataType.Float64);
|
||||
HistorianDataTypes.FromKind(HistorianValueKind.Double).ShouldBe((byte)DriverDataType.Float64);
|
||||
HistorianDataTypes.FromKind(HistorianValueKind.Boolean).ShouldBe((byte)DriverDataType.Boolean);
|
||||
HistorianDataTypes.FromKind(HistorianValueKind.Empty).ShouldBe((byte)DriverDataType.Float64);
|
||||
}
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
<RootNamespace>ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.Tests</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="xunit.v3"/>
|
||||
<PackageReference Include="Shouldly"/>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk"/>
|
||||
<!-- SDK oracle for the status-code / aggregate-bit tests. -->
|
||||
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Core"/>
|
||||
<PackageReference Include="xunit.runner.visualstudio">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\..\src\Drivers\ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common\ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common.csproj"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
|
||||
|
||||
-7
@@ -360,13 +360,6 @@ public sealed class ProcessedReadTests(SqlHistorianFixture fx)
|
||||
src.GetHealthSnapshot().TotalQueries.ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Every_enum_member_is_either_served_or_refused()
|
||||
{
|
||||
// Thirteen served; anything appended to the enum later falls to the refusal (total default).
|
||||
Enum.GetValues<HistoryAggregateType>().Count(a => SqlAggregateQueries.FamilyOf(a) is not null).ShouldBe(13);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task The_bucket_count_is_capped_at_100000()
|
||||
{
|
||||
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
|
||||
@@ -96,8 +97,8 @@ public sealed class QualityContractTests(SqlHistorianFixture fx)
|
||||
new(At(2), HistorianValue.Empty, BadNoCommunication),
|
||||
new(At(3), HistorianValue.FromDouble(1), 0u),
|
||||
], Ct);
|
||||
r.ShouldBe([BadInvalidArgument, BadInvalidArgument, SqlHistorianStatusCodes.GoodEntryInserted,
|
||||
SqlHistorianStatusCodes.GoodEntryInserted]);
|
||||
r.ShouldBe([BadInvalidArgument, BadInvalidArgument, HistorianStatusCodes.GoodEntryInserted,
|
||||
HistorianStatusCodes.GoodEntryInserted]);
|
||||
(await db.Source().ReadRawAsync("u", At(0), At(10), 0, Ct)).Samples
|
||||
.Select(s => s.StatusCode).ShouldBe([BadNoCommunication, 0u]);
|
||||
}
|
||||
|
||||
+4
-3
@@ -2,6 +2,7 @@ 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;
|
||||
|
||||
@@ -59,7 +60,7 @@ internal sealed class ReadTestDb
|
||||
}
|
||||
foreach (var (t, v, status) in samples)
|
||||
{
|
||||
var col = SampleRowMapper.ToColumns(v);
|
||||
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)");
|
||||
@@ -69,7 +70,7 @@ internal sealed class ReadTestDb
|
||||
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", SampleRowMapper.ToDbStatus(status));
|
||||
cmd.Parameters.AddWithValue("@st", SampleColumnMapper.ToDbStatus(status));
|
||||
SqlHistorianConnection.AddDateTime2(cmd, "@srv", t.AddSeconds(1));
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
}
|
||||
@@ -253,7 +254,7 @@ public sealed class RawReadTests(SqlHistorianFixture fx)
|
||||
// Another node creates the schema; the same data source then succeeds (bootstrap failure is not latched).
|
||||
var creator = new SqlServerHistorianOptions { ConnectionString = o.ConnectionString, Schema = o.Schema };
|
||||
await new SqlServerSchemaBootstrapper(new SqlHistorianConnection(creator), creator).EnsureAsync(Ct);
|
||||
clock.Now += SqlServerSchemaBootstrapper.FailureCacheDuration; // past the 5 s failure cache (M-2)
|
||||
clock.Now += BootstrapFailureCache.DefaultDuration; // past the 5 s failure cache (M-2)
|
||||
(await src.ReadRawAsync("A", At(0), At(1), 0, Ct)).Samples.ShouldBeEmpty();
|
||||
src.GetHealthSnapshot().ConsecutiveFailures.ShouldBe(0);
|
||||
}
|
||||
|
||||
+5
-4
@@ -1,6 +1,7 @@
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests;
|
||||
|
||||
@@ -82,12 +83,12 @@ public sealed class SchemaBootstrapIntegrationTests(SqlHistorianFixture fx)
|
||||
var checker = new SqlServerSchemaBootstrapper(new SqlHistorianConnection(o), o, timeProvider: clock);
|
||||
await Should.ThrowAsync<HistorianSchemaMissingException>(() => checker.EnsureAsync(CancellationToken.None));
|
||||
await Boot(Opts(s)).EnsureAsync(CancellationToken.None);
|
||||
clock.Now += SqlServerSchemaBootstrapper.FailureCacheDuration;
|
||||
clock.Now += BootstrapFailureCache.DefaultDuration;
|
||||
await checker.EnsureAsync(CancellationToken.None); // same instance retries once the failure cache expires
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// M-2: within <see cref="SqlServerSchemaBootstrapper.FailureCacheDuration"/> of a failure the bootstrapper
|
||||
/// M-2: within <see cref="BootstrapFailureCache.DefaultDuration"/> of a failure the bootstrapper
|
||||
/// re-throws it without touching SQL — proven by creating the schema in between: a call that went to SQL
|
||||
/// would now pass.
|
||||
/// </summary>
|
||||
@@ -103,7 +104,7 @@ public sealed class SchemaBootstrapIntegrationTests(SqlHistorianFixture fx)
|
||||
|
||||
await Boot(Opts(s)).EnsureAsync(CancellationToken.None); // the schema now exists
|
||||
|
||||
clock.Now += SqlServerSchemaBootstrapper.FailureCacheDuration - TimeSpan.FromTicks(1);
|
||||
clock.Now += BootstrapFailureCache.DefaultDuration - TimeSpan.FromTicks(1);
|
||||
await Should.ThrowAsync<HistorianSchemaMissingException>(() => checker.EnsureAsync(CancellationToken.None));
|
||||
|
||||
clock.Now += TimeSpan.FromTicks(1);
|
||||
@@ -156,7 +157,7 @@ public sealed class SchemaBootstrapIntegrationTests(SqlHistorianFixture fx)
|
||||
"SELECT @id, SourceTimeUtc, ValueKind, NumValue, IntValue, StrValue, StatusCode, SYSUTCDATETIME() FROM @rows");
|
||||
cmd.Parameters.AddWithValue("@id", tagId);
|
||||
conn.AddStructured(cmd, "@rows", "SampleRow",
|
||||
SampleRowMapper.ToSampleRecords([new SqlSampleRow(t, ZB.MOM.WW.OtOpcUa.Core.Abstractions.HistorianValue.FromDouble(1), 0u)]));
|
||||
SampleRowMapper.ToSampleRecords([new HistorianSampleRow(t, ZB.MOM.WW.OtOpcUa.Core.Abstractions.HistorianValue.FromDouble(1), 0u)]));
|
||||
await cmd.ExecuteNonQueryAsync();
|
||||
await using var q = conn.CreateCommand(c, $"SELECT SourceTimeUtc FROM {conn.Q("Sample")}");
|
||||
await using var r = await q.ExecuteReaderAsync();
|
||||
|
||||
+24
-23
@@ -2,14 +2,15 @@ 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;
|
||||
|
||||
[Collection(SqlHistorianCollection.Name)]
|
||||
public sealed class UpdateTests(SqlHistorianFixture fx)
|
||||
{
|
||||
private const uint Good = SqlHistorianStatusCodes.Good;
|
||||
private const uint UncertainStatus = SqlHistorianStatusCodes.UncertainDataSubNormal;
|
||||
private const uint Good = HistorianStatusCodes.Good;
|
||||
private const uint UncertainStatus = HistorianStatusCodes.UncertainDataSubNormal;
|
||||
|
||||
// Deliberately NOT millisecond aligned: sub-ms ticks must survive the TVP, MERGE join and DELETE join.
|
||||
private static readonly DateTime T0 = new(2026, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
@@ -58,10 +59,10 @@ public sealed class UpdateTests(SqlHistorianFixture fx)
|
||||
{
|
||||
var (u, s) = Make();
|
||||
var r1 = await u.UpdateDataAsync("t", HistoryUpdateMode.Insert, [V(T(1), 1), V(T(2), 2)], Ct);
|
||||
r1.ShouldBe([SqlHistorianStatusCodes.GoodEntryInserted, SqlHistorianStatusCodes.GoodEntryInserted]);
|
||||
r1.ShouldBe([HistorianStatusCodes.GoodEntryInserted, HistorianStatusCodes.GoodEntryInserted]);
|
||||
// Mixed: T(2) exists, T(3) new, T(1) exists - out of timestamp order on purpose.
|
||||
var r2 = await u.UpdateDataAsync("t", HistoryUpdateMode.Insert, [V(T(3), 3), V(T(2), 99), V(T(1), 98)], Ct);
|
||||
r2.ShouldBe([SqlHistorianStatusCodes.GoodEntryInserted, SqlHistorianStatusCodes.BadEntryExists, SqlHistorianStatusCodes.BadEntryExists]);
|
||||
r2.ShouldBe([HistorianStatusCodes.GoodEntryInserted, HistorianStatusCodes.BadEntryExists, HistorianStatusCodes.BadEntryExists]);
|
||||
var rows = await RowsAsync(s, "t");
|
||||
rows.Select(x => x.Num).ShouldBe([1d, 2d, 3d]); // existing values untouched
|
||||
rows.Select(x => x.Time).ShouldBe([T(1), T(2), T(3)]); // sub-ms ticks preserved
|
||||
@@ -74,7 +75,7 @@ public sealed class UpdateTests(SqlHistorianFixture fx)
|
||||
await u.UpdateDataAsync("t", HistoryUpdateMode.Insert, [V(T(1), 1), V(T(2), 2)], Ct);
|
||||
var r = await u.UpdateDataAsync("t", HistoryUpdateMode.Replace,
|
||||
[V(T(2), 22, UncertainStatus), V(T(5), 5), V(T(1), 11)], Ct);
|
||||
r.ShouldBe([SqlHistorianStatusCodes.GoodEntryReplaced, SqlHistorianStatusCodes.BadNoEntryExists, SqlHistorianStatusCodes.GoodEntryReplaced]);
|
||||
r.ShouldBe([HistorianStatusCodes.GoodEntryReplaced, HistorianStatusCodes.BadNoEntryExists, HistorianStatusCodes.GoodEntryReplaced]);
|
||||
var rows = await RowsAsync(s, "t");
|
||||
rows.Count.ShouldBe(2); // T(5) was not created
|
||||
rows[0].Num.ShouldBe(11d);
|
||||
@@ -89,7 +90,7 @@ public sealed class UpdateTests(SqlHistorianFixture fx)
|
||||
var (u, s) = Make();
|
||||
await u.UpdateDataAsync("t", HistoryUpdateMode.Insert, [V(T(1), 1)], Ct);
|
||||
var r = await u.UpdateDataAsync("t", HistoryUpdateMode.Update, [V(T(2), 2), V(T(1), 10)], Ct);
|
||||
r.ShouldBe([SqlHistorianStatusCodes.GoodEntryInserted, SqlHistorianStatusCodes.GoodEntryReplaced]);
|
||||
r.ShouldBe([HistorianStatusCodes.GoodEntryInserted, HistorianStatusCodes.GoodEntryReplaced]);
|
||||
(await RowsAsync(s, "t")).Select(x => x.Num).ShouldBe([10d, 2d]);
|
||||
}
|
||||
|
||||
@@ -98,13 +99,13 @@ public sealed class UpdateTests(SqlHistorianFixture fx)
|
||||
{
|
||||
var (u, s) = Make();
|
||||
(await u.UpdateDataAsync("rep", HistoryUpdateMode.Replace, [V(T(1), 1)], Ct))
|
||||
.ShouldBe([SqlHistorianStatusCodes.BadNoEntryExists]);
|
||||
(await u.DeleteRawAsync("del", T(0), T(9), Ct)).ShouldBe(SqlHistorianStatusCodes.BadNoData);
|
||||
(await u.DeleteAtTimeAsync("del2", [T(1)], Ct)).ShouldBe([SqlHistorianStatusCodes.BadNoEntryExists]);
|
||||
.ShouldBe([HistorianStatusCodes.BadNoEntryExists]);
|
||||
(await u.DeleteRawAsync("del", T(0), T(9), Ct)).ShouldBe(HistorianStatusCodes.BadNoData);
|
||||
(await u.DeleteAtTimeAsync("del2", [T(1)], Ct)).ShouldBe([HistorianStatusCodes.BadNoEntryExists]);
|
||||
(await TagCountAsync(s)).ShouldBe(0); // none of those created a tag
|
||||
|
||||
(await u.UpdateDataAsync("ins", HistoryUpdateMode.Insert, [V(T(1), 1)], Ct)).ShouldBe([SqlHistorianStatusCodes.GoodEntryInserted]);
|
||||
(await u.UpdateDataAsync("upd", HistoryUpdateMode.Update, [V(T(1), 1)], Ct)).ShouldBe([SqlHistorianStatusCodes.GoodEntryInserted]);
|
||||
(await u.UpdateDataAsync("ins", HistoryUpdateMode.Insert, [V(T(1), 1)], Ct)).ShouldBe([HistorianStatusCodes.GoodEntryInserted]);
|
||||
(await u.UpdateDataAsync("upd", HistoryUpdateMode.Update, [V(T(1), 1)], Ct)).ShouldBe([HistorianStatusCodes.GoodEntryInserted]);
|
||||
(await TagCountAsync(s)).ShouldBe(2);
|
||||
}
|
||||
|
||||
@@ -114,13 +115,13 @@ public sealed class UpdateTests(SqlHistorianFixture fx)
|
||||
var (u, s) = Make();
|
||||
var r = await u.UpdateDataAsync("t", HistoryUpdateMode.Insert,
|
||||
[V(null, 1), V(T(1), 2), V(DateTime.MinValue, 3)], Ct);
|
||||
r.ShouldBe([SqlHistorianStatusCodes.BadInvalidTimestamp, SqlHistorianStatusCodes.GoodEntryInserted, SqlHistorianStatusCodes.BadInvalidTimestamp]);
|
||||
r.ShouldBe([HistorianStatusCodes.BadInvalidTimestamp, HistorianStatusCodes.GoodEntryInserted, HistorianStatusCodes.BadInvalidTimestamp]);
|
||||
(await RowsAsync(s, "t")).Count.ShouldBe(1);
|
||||
|
||||
// All-invalid request: no DB work, no tag created, still one code per item.
|
||||
var (u2, s2) = Make();
|
||||
await u2.UpdateDataAsync("seed", HistoryUpdateMode.Insert, [V(T(1), 1)], Ct); // bootstraps the schema
|
||||
(await u2.UpdateDataAsync("x", HistoryUpdateMode.Insert, [V(null, 1)], Ct)).ShouldBe([SqlHistorianStatusCodes.BadInvalidTimestamp]);
|
||||
(await u2.UpdateDataAsync("x", HistoryUpdateMode.Insert, [V(null, 1)], Ct)).ShouldBe([HistorianStatusCodes.BadInvalidTimestamp]);
|
||||
(await TagCountAsync(s2)).ShouldBe(1); // only "seed"; "x" was never created
|
||||
}
|
||||
|
||||
@@ -130,8 +131,8 @@ public sealed class UpdateTests(SqlHistorianFixture fx)
|
||||
var (u, s) = Make();
|
||||
var r = await u.UpdateDataAsync("t", HistoryUpdateMode.Insert,
|
||||
[V(T(1), double.NaN), V(T(2), 2), V(T(3), double.PositiveInfinity), V(T(4), double.NegativeInfinity)], Ct);
|
||||
r.ShouldBe([SqlHistorianUpdateStatusCodes.BadInvalidArgument, SqlHistorianStatusCodes.GoodEntryInserted,
|
||||
SqlHistorianUpdateStatusCodes.BadInvalidArgument, SqlHistorianUpdateStatusCodes.BadInvalidArgument]);
|
||||
r.ShouldBe([HistorianStatusCodes.BadInvalidArgument, HistorianStatusCodes.GoodEntryInserted,
|
||||
HistorianStatusCodes.BadInvalidArgument, HistorianStatusCodes.BadInvalidArgument]);
|
||||
(await RowsAsync(s, "t")).Select(x => x.Num).ShouldBe([2d]);
|
||||
}
|
||||
|
||||
@@ -140,7 +141,7 @@ public sealed class UpdateTests(SqlHistorianFixture fx)
|
||||
{
|
||||
var (u, s) = Make();
|
||||
var r = await u.UpdateDataAsync("t", HistoryUpdateMode.Insert, [V(T(1), 1), V(T(2), 2), V(T(1), 99)], Ct);
|
||||
r.ShouldBe([SqlHistorianStatusCodes.GoodEntryInserted, SqlHistorianStatusCodes.GoodEntryInserted, SqlHistorianStatusCodes.BadEntryExists]);
|
||||
r.ShouldBe([HistorianStatusCodes.GoodEntryInserted, HistorianStatusCodes.GoodEntryInserted, HistorianStatusCodes.BadEntryExists]);
|
||||
(await RowsAsync(s, "t")).Select(x => x.Num).ShouldBe([1d, 2d]);
|
||||
}
|
||||
|
||||
@@ -150,11 +151,11 @@ public sealed class UpdateTests(SqlHistorianFixture fx)
|
||||
var (u, s) = Make();
|
||||
await u.UpdateDataAsync("t", HistoryUpdateMode.Insert, [V(T(1), 1)], Ct);
|
||||
var rep = await u.UpdateDataAsync("t", HistoryUpdateMode.Replace, [V(T(1), 5), V(T(1), 6)], Ct);
|
||||
rep.ShouldBe([SqlHistorianStatusCodes.GoodEntryReplaced, SqlHistorianStatusCodes.GoodEntryReplaced]);
|
||||
rep.ShouldBe([HistorianStatusCodes.GoodEntryReplaced, HistorianStatusCodes.GoodEntryReplaced]);
|
||||
(await RowsAsync(s, "t")).Single().Num.ShouldBe(6d);
|
||||
|
||||
var upd = await u.UpdateDataAsync("t", HistoryUpdateMode.Update, [V(T(7), 7), V(T(7), 8)], Ct);
|
||||
upd.ShouldBe([SqlHistorianStatusCodes.GoodEntryInserted, SqlHistorianStatusCodes.GoodEntryInserted]);
|
||||
upd.ShouldBe([HistorianStatusCodes.GoodEntryInserted, HistorianStatusCodes.GoodEntryInserted]);
|
||||
(await RowsAsync(s, "t")).Last().Num.ShouldBe(8d);
|
||||
}
|
||||
|
||||
@@ -164,7 +165,7 @@ public sealed class UpdateTests(SqlHistorianFixture fx)
|
||||
var (u, s) = Make();
|
||||
await u.UpdateDataAsync("t", HistoryUpdateMode.Insert, [new HistorizationValue(T(1), HistorianValue.FromDouble(1.5), Good)], Ct);
|
||||
(await u.UpdateDataAsync("t", HistoryUpdateMode.Replace,
|
||||
[new HistorizationValue(T(1), HistorianValue.FromString("hi"), Good)], Ct)).ShouldBe([SqlHistorianStatusCodes.GoodEntryReplaced]);
|
||||
[new HistorizationValue(T(1), HistorianValue.FromString("hi"), Good)], Ct)).ShouldBe([HistorianStatusCodes.GoodEntryReplaced]);
|
||||
var rows = await RowsAsync(s, "t");
|
||||
rows.Single().Num.ShouldBeNull(); // NumValue cleared by the String replace
|
||||
}
|
||||
@@ -177,9 +178,9 @@ public sealed class UpdateTests(SqlHistorianFixture fx)
|
||||
// [T(1), T(3)) removes T(1) and T(2); T(3) (the end) survives.
|
||||
(await u.DeleteRawAsync("t", T(1), T(3), Ct)).ShouldBe(Good);
|
||||
(await RowsAsync(s, "t")).Select(x => x.Time).ShouldBe([T(3)]);
|
||||
(await u.DeleteRawAsync("t", T(1), T(3), Ct)).ShouldBe(SqlHistorianStatusCodes.BadNoData);
|
||||
(await u.DeleteRawAsync("t", T(1), T(3), Ct)).ShouldBe(HistorianStatusCodes.BadNoData);
|
||||
// sub-ms boundary: a start one tick after T(3) excludes it.
|
||||
(await u.DeleteRawAsync("t", T(3).AddTicks(1), T(3).AddTicks(100), Ct)).ShouldBe(SqlHistorianStatusCodes.BadNoData);
|
||||
(await u.DeleteRawAsync("t", T(3).AddTicks(1), T(3).AddTicks(100), Ct)).ShouldBe(HistorianStatusCodes.BadNoData);
|
||||
(await RowsAsync(s, "t")).Count.ShouldBe(1);
|
||||
}
|
||||
|
||||
@@ -189,7 +190,7 @@ public sealed class UpdateTests(SqlHistorianFixture fx)
|
||||
var (u, s) = Make();
|
||||
await u.UpdateDataAsync("t", HistoryUpdateMode.Insert, [V(T(1), 1), V(T(2), 2), V(T(3), 3)], Ct);
|
||||
var r = await u.DeleteAtTimeAsync("t", [T(3), T(2).AddTicks(1) /* near miss */, T(1), DateTime.MinValue], Ct);
|
||||
r.ShouldBe([Good, SqlHistorianStatusCodes.BadNoEntryExists, Good, SqlHistorianStatusCodes.BadInvalidTimestamp]);
|
||||
r.ShouldBe([Good, HistorianStatusCodes.BadNoEntryExists, Good, HistorianStatusCodes.BadInvalidTimestamp]);
|
||||
(await RowsAsync(s, "t")).Select(x => x.Time).ShouldBe([T(2)]);
|
||||
}
|
||||
|
||||
@@ -198,7 +199,7 @@ public sealed class UpdateTests(SqlHistorianFixture fx)
|
||||
{
|
||||
var (u, s) = Make();
|
||||
await u.UpdateDataAsync("t", HistoryUpdateMode.Insert, [V(T(1), 1)], Ct);
|
||||
(await u.DeleteAtTimeAsync("t", [T(1), T(1)], Ct)).ShouldBe([Good, SqlHistorianStatusCodes.BadNoEntryExists]);
|
||||
(await u.DeleteAtTimeAsync("t", [T(1), T(1)], Ct)).ShouldBe([Good, HistorianStatusCodes.BadNoEntryExists]);
|
||||
(await RowsAsync(s, "t")).ShouldBeEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
+5
-4
@@ -3,6 +3,7 @@ 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;
|
||||
|
||||
@@ -55,11 +56,11 @@ public sealed class WriteTests(SqlHistorianFixture fx)
|
||||
var rows = await RowsAsync(s, "tag.a");
|
||||
rows.Count.ShouldBe(6);
|
||||
for (var i = 0; i < 6; i++) rows[i].T.Ticks.ShouldBe(vals[i].TimestampUtc!.Value.Ticks);
|
||||
var back = rows.Select(r => SampleRowMapper.FromColumns(r.Kind, r.N, r.I, r.S)).ToList();
|
||||
var back = rows.Select(r => SampleColumnMapper.FromColumns(r.Kind, r.N, r.I, r.S)).ToList();
|
||||
back.ShouldBe(vals.Select(v => v.Value).ToList());
|
||||
(rows[5].N, rows[5].I, rows[5].S).ShouldBe((null, null, null));
|
||||
rows[1].Status.ShouldBe(unchecked((int)0x40000000u));
|
||||
SampleRowMapper.FromDbStatus(rows[5].Status).ShouldBe(0x80310000u);
|
||||
SampleColumnMapper.FromDbStatus(rows[5].Status).ShouldBe(0x80310000u);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
@@ -101,8 +102,8 @@ public sealed class WriteTests(SqlHistorianFixture fx)
|
||||
rows.Count.ShouldBe(3);
|
||||
rows[0].Kind.ShouldBe((byte)HistorianValueKind.Empty);
|
||||
rows[0].N.ShouldBeNull();
|
||||
rows[0].Status.ShouldBe(unchecked((int)SqlHistorianStatusCodes.BadOutOfRange)); // Uncertain NaN ⇒ Bad (I-1)
|
||||
rows[1].Status.ShouldBe(unchecked((int)SqlHistorianStatusCodes.BadOutOfRange)); // Good +Inf ⇒ Bad (I-1)
|
||||
rows[0].Status.ShouldBe(unchecked((int)HistorianStatusCodes.BadOutOfRange)); // Uncertain NaN ⇒ Bad (I-1)
|
||||
rows[1].Status.ShouldBe(unchecked((int)HistorianStatusCodes.BadOutOfRange)); // Good +Inf ⇒ Bad (I-1)
|
||||
rows[2].N.ShouldBe(7);
|
||||
}
|
||||
|
||||
|
||||
+10
-9
@@ -2,6 +2,7 @@ using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
|
||||
using ZB.MOM.WW.OtOpcUa.Core.AlarmHistorian;
|
||||
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Sql.Common;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.Tests;
|
||||
|
||||
@@ -16,8 +17,8 @@ public sealed class AlarmEventIdTests
|
||||
[Fact]
|
||||
public void Id_is_stable_lowercase_hex_sha256()
|
||||
{
|
||||
var a = SqlServerAlarmHistorianWriter.ComputeEventId(Ev());
|
||||
a.ShouldBe(SqlServerAlarmHistorianWriter.ComputeEventId(Ev()));
|
||||
var a = AlarmEventIds.ComputeEventId(Ev());
|
||||
a.ShouldBe(AlarmEventIds.ComputeEventId(Ev()));
|
||||
a.Length.ShouldBe(64);
|
||||
a.ShouldBe(a.ToLowerInvariant());
|
||||
}
|
||||
@@ -26,23 +27,23 @@ public sealed class AlarmEventIdTests
|
||||
public void Field_boundaries_are_unambiguous()
|
||||
{
|
||||
// The delimiter-based scheme collided on these; length prefixes must not.
|
||||
SqlServerAlarmHistorianWriter.ComputeEventId(Ev(path: "a|b", id: "c"))
|
||||
.ShouldNotBe(SqlServerAlarmHistorianWriter.ComputeEventId(Ev(path: "a", id: "b|c")));
|
||||
SqlServerAlarmHistorianWriter.ComputeEventId(Ev(path: "ab", id: "c"))
|
||||
.ShouldNotBe(SqlServerAlarmHistorianWriter.ComputeEventId(Ev(path: "a", id: "bc")));
|
||||
AlarmEventIds.ComputeEventId(Ev(path: "a|b", id: "c"))
|
||||
.ShouldNotBe(AlarmEventIds.ComputeEventId(Ev(path: "a", id: "b|c")));
|
||||
AlarmEventIds.ComputeEventId(Ev(path: "ab", id: "c"))
|
||||
.ShouldNotBe(AlarmEventIds.ComputeEventId(Ev(path: "a", id: "bc")));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Null_and_empty_differ_and_every_field_participates()
|
||||
{
|
||||
var basis = SqlServerAlarmHistorianWriter.ComputeEventId(Ev());
|
||||
SqlServerAlarmHistorianWriter.ComputeEventId(Ev(comment: null)).ShouldNotBe(SqlServerAlarmHistorianWriter.ComputeEventId(Ev(comment: "")));
|
||||
var basis = AlarmEventIds.ComputeEventId(Ev());
|
||||
AlarmEventIds.ComputeEventId(Ev(comment: null)).ShouldNotBe(AlarmEventIds.ComputeEventId(Ev(comment: "")));
|
||||
foreach (var other in new[]
|
||||
{
|
||||
Ev(path: "Q"), Ev(id: "B"), Ev(kind: "Cleared"), Ev(user: "v"), Ev(comment: "d"), Ev(msg: "x"),
|
||||
Ev(t: T.AddTicks(1)),
|
||||
})
|
||||
SqlServerAlarmHistorianWriter.ComputeEventId(other).ShouldNotBe(basis);
|
||||
AlarmEventIds.ComputeEventId(other).ShouldNotBe(basis);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
|
||||
+3
-37
@@ -2,52 +2,18 @@ using Microsoft.Data.SqlClient.Server;
|
||||
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.Tests;
|
||||
|
||||
/// <summary>The TVP half of the mapper tests (the column round-trip half moved to Sql.Common.Tests).</summary>
|
||||
public sealed class SampleRowMapperTests
|
||||
{
|
||||
public static TheoryData<HistorianValue> Values => new()
|
||||
{
|
||||
HistorianValue.FromDouble(12.5),
|
||||
HistorianValue.FromInt64(long.MinValue + 3),
|
||||
HistorianValue.FromBoolean(true),
|
||||
HistorianValue.FromBoolean(false),
|
||||
HistorianValue.FromString("héllo"),
|
||||
HistorianValue.FromString(""),
|
||||
HistorianValue.FromDateTime(new DateTime(2026, 10, 9, 1, 2, 3, DateTimeKind.Utc).AddTicks(1234567)),
|
||||
HistorianValue.Empty,
|
||||
};
|
||||
|
||||
[Theory]
|
||||
[MemberData(nameof(Values))]
|
||||
public void Every_kind_round_trips_through_the_column_quad(HistorianValue v)
|
||||
{
|
||||
var c = SampleRowMapper.ToColumns(v);
|
||||
SampleRowMapper.FromColumns(c.ValueKind, c.NumValue, c.IntValue, c.StrValue).ShouldBe(v);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Columns_follow_the_spec_mapping()
|
||||
{
|
||||
SampleRowMapper.ToColumns(HistorianValue.FromInt64(7)).ShouldBe(new SampleColumns(2, 7d, 7L, null));
|
||||
SampleRowMapper.ToColumns(HistorianValue.FromBoolean(true)).ShouldBe(new SampleColumns(3, 1d, 1L, null));
|
||||
SampleRowMapper.ToColumns(HistorianValue.FromDouble(1.5)).ShouldBe(new SampleColumns(1, 1.5, null, null));
|
||||
SampleRowMapper.ToColumns(HistorianValue.Empty).ShouldBe(new SampleColumns(0, null, null, null));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Status_is_stored_bit_for_bit()
|
||||
{
|
||||
SampleRowMapper.ToDbStatus(0x80050000u).ShouldBeLessThan(0);
|
||||
SampleRowMapper.FromDbStatus(SampleRowMapper.ToDbStatus(0x80050000u)).ShouldBe(0x80050000u);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sample_records_keep_sub_millisecond_ticks()
|
||||
{
|
||||
var t = new DateTime(2026, 10, 9, 0, 0, 0, DateTimeKind.Utc).AddTicks(1234567);
|
||||
var rec = SampleRowMapper.ToSampleRecords([new SqlSampleRow(t, HistorianValue.FromDouble(2), 0x80050000u)]).Single();
|
||||
var rec = SampleRowMapper.ToSampleRecords([new HistorianSampleRow(t, HistorianValue.FromDouble(2), 0x80050000u)]).Single();
|
||||
rec.GetDateTime(0).Ticks.ShouldBe(t.Ticks);
|
||||
rec.GetByte(1).ShouldBe((byte)1);
|
||||
rec.GetDouble(2).ShouldBe(2d);
|
||||
|
||||
Reference in New Issue
Block a user