feat(historian): SqlServerHistorianProvider + SqlServer backend registration

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-09 13:42:38 -04:00
parent a611cc358f
commit 7500106c2f
7 changed files with 181 additions and 0 deletions
@@ -0,0 +1,39 @@
using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.Tests;
public sealed class SqlServerHistorianProviderTests
{
private static SqlServerHistorianProvider Make() => new(new SqlServerHistorianOptions
{
ConnectionString = "Server=localhost;User Id=sa;Password=x;TrustServerCertificate=True",
});
[Fact]
public void Name_and_all_six_kinds()
{
var p = Make();
p.Name.ShouldBe("SqlServer");
p.SupportedKinds.ShouldBe(Enum.GetValues<HistorianValueKind>(), ignoreOrder: true);
p.SupportedKinds.Count.ShouldBe(6);
}
[Fact]
public void Creates_all_five_seams_without_touching_sql()
{
var sp = new NullSp();
var p = Make();
p.CreateDataSource(sp).ShouldBeOfType<SqlServerHistorianDataSource>();
p.CreateValueWriter(sp).ShouldBeOfType<SqlServerHistorianValueWriter>();
p.CreateAlarmWriter(sp).ShouldBeOfType<SqlServerAlarmHistorianWriter>();
p.CreateProvisioning(sp).ShouldBeOfType<SqlServerTagProvisioner>();
p.CreateUpdater(sp).ShouldBeOfType<SqlServerHistorianDataUpdater>();
}
private sealed class NullSp : IServiceProvider
{
public object? GetService(Type serviceType) => null;
}
}
@@ -306,4 +306,37 @@ public sealed class ServerHistorianOptionsValidatorTests
() => _ = provider.GetRequiredService<IOptions<ServerHistorianOptions>>().Value);
ex.Failures.ShouldContain(f => f.Contains("ServerHistorian:Endpoint"));
}
private static ServerHistorianOptionsValidator SqlSut(string? connectionString)
{
var values = new Dictionary<string, string?> { ["AlarmHistorian:Enabled"] = "false" };
if (connectionString is not null) values["ServerHistorian:SqlServer:ConnectionString"] = connectionString;
return new ServerHistorianOptionsValidator(new ConfigurationBuilder().AddInMemoryCollection(values).Build());
}
/// <summary>SqlServer with a connection string passes; the gateway Endpoint/ApiKey/UseTls checks do not fire.</summary>
[Fact]
public void SqlServer_backend_with_connection_string_passes_without_gateway_settings()
{
var result = SqlSut("Server=localhost;Database=h").Validate(null,
new ServerHistorianOptions { Backend = "SqlServer", Enabled = true });
result.Failed.ShouldBeFalse(string.Join("; ", result.Failures ?? []));
}
/// <summary>SqlServer with no connection string fails, naming the key.</summary>
[Fact]
public void SqlServer_backend_without_connection_string_fails_naming_the_key()
{
var result = SqlSut(null).Validate(null, new ServerHistorianOptions { Backend = "SqlServer", Enabled = true });
result.Failed.ShouldBeTrue();
result.Failures.ShouldContain(f => f.Contains("ServerHistorian:SqlServer:ConnectionString"));
result.Failures.ShouldNotContain(f => f.Contains("ServerHistorian:Endpoint"));
}
/// <summary>Unconsumed SqlServer section (nothing enabled) is not validated.</summary>
[Fact]
public void SqlServer_backend_not_consumed_passes_without_connection_string() =>
SqlSut(null).Validate(null, new ServerHistorianOptions { Backend = "SqlServer" }).Failed.ShouldBeFalse();
}
@@ -5,6 +5,8 @@ using Shouldly;
using Xunit;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.Gateway.Recorder;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer;
using ZB.MOM.WW.OtOpcUa.Host.Configuration;
using ZB.MOM.WW.OtOpcUa.Runtime.Historian;
@@ -59,4 +61,29 @@ public sealed class HistorianBackendRegistryTests
p.CreateProvisioning(sp).ShouldBeOfType<GatewayTagProvisioner>();
p.CreateUpdater(sp).ShouldBeNull();
}
private static IConfiguration SqlCfg() => new ConfigurationBuilder().AddInMemoryCollection(new Dictionary<string, string?>
{
["ServerHistorian:SqlServer:ConnectionString"] = "Server=localhost;User Id=sa;Password=x;TrustServerCertificate=True",
}).Build();
[Fact]
public void SqlServer_backend_resolves_and_builds_the_sql_seams()
{
var sp = new ServiceCollection().BuildServiceProvider();
var p = HistorianBackendRegistry.Resolve(new ServerHistorianOptions { Backend = "sqlserver", Enabled = true }, SqlCfg());
p.ShouldBeOfType<SqlServerHistorianProvider>();
HistorianBackendRegistry.KnownBackends.ShouldContain("SqlServer");
p.SupportedKinds.Count.ShouldBe(6);
p.SupportedKinds.ShouldContain(HistorianValueKind.String);
p.SupportedKinds.ShouldContain(HistorianValueKind.Empty);
p.CreateDataSource(sp).ShouldBeOfType<SqlServerHistorianDataSource>();
p.CreateValueWriter(sp).GetType().Name.ShouldBe("SqlServerHistorianValueWriter");
p.CreateAlarmWriter(sp).GetType().Name.ShouldBe("SqlServerAlarmHistorianWriter");
p.CreateProvisioning(sp).GetType().Name.ShouldBe("SqlServerTagProvisioner");
var holder = new HistorianUpdaterHolder(p.CreateUpdater(sp));
holder.Updater.ShouldNotBeNull();
holder.Updater!.GetType().Name.ShouldBe("SqlServerHistorianDataUpdater");
}
}