feat(historian): SQL Server backend skeleton - schema DDL, locked bootstrapper, options, connection/mapper/tag-catalog helpers, status consts, env-gated fixture
Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
+79
@@ -0,0 +1,79 @@
|
||||
using Microsoft.Data.SqlClient;
|
||||
using Xunit;
|
||||
|
||||
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer.IntegrationTests;
|
||||
|
||||
/// <summary>
|
||||
/// Env-gated throwaway database for the SQL historian backend. <c>HISTORIAN_SQL_TEST_CONNECTION</c> absent ⇒
|
||||
/// <see cref="SkipReason"/> is set, nothing connects, and every test calls <c>Assert.Skip</c>. Present ⇒
|
||||
/// creates <c>OtOpcUaHistorian_T_<guid></c> (never touches another database) and drops it on dispose.
|
||||
/// Tests isolate from each other with <see cref="UniqueSchema"/>.
|
||||
/// </summary>
|
||||
public sealed class SqlHistorianFixture : IAsyncLifetime
|
||||
{
|
||||
/// <summary>Env var holding a connection string with CREATE/DROP DATABASE rights.</summary>
|
||||
public const string ConnectionEnvVar = "HISTORIAN_SQL_TEST_CONNECTION";
|
||||
|
||||
private string? _master;
|
||||
private string? _dbName;
|
||||
|
||||
/// <summary>Why tests must skip, or null when the database is available.</summary>
|
||||
public string? SkipReason { get; private set; }
|
||||
|
||||
/// <summary>Connection string targeting the throwaway database. Valid only when <see cref="SkipReason"/> is null.</summary>
|
||||
public string ConnectionString { get; private set; } = "";
|
||||
|
||||
/// <summary>Skips the calling test when the gate is closed.</summary>
|
||||
public void SkipIfUnavailable()
|
||||
{
|
||||
if (SkipReason is not null) Assert.Skip(SkipReason);
|
||||
}
|
||||
|
||||
/// <summary>A fresh, valid schema name (nothing exists under it yet).</summary>
|
||||
/// <returns>A schema identifier unique to this call.</returns>
|
||||
public static string UniqueSchema() => "h" + Guid.NewGuid().ToString("N")[..16];
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask InitializeAsync()
|
||||
{
|
||||
var cs = Environment.GetEnvironmentVariable(ConnectionEnvVar);
|
||||
if (string.IsNullOrWhiteSpace(cs))
|
||||
{
|
||||
SkipReason = $"{ConnectionEnvVar} is not set; SQL Server historian integration tests skipped.";
|
||||
return;
|
||||
}
|
||||
|
||||
var b = new SqlConnectionStringBuilder(cs) { InitialCatalog = "master" };
|
||||
_master = b.ConnectionString;
|
||||
_dbName = "OtOpcUaHistorian_T_" + Guid.NewGuid().ToString("N");
|
||||
await ExecAsync(_master, $"CREATE DATABASE [{_dbName}]").ConfigureAwait(false);
|
||||
b.InitialCatalog = _dbName;
|
||||
ConnectionString = b.ConnectionString;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_master is null || _dbName is null) return;
|
||||
SqlConnection.ClearAllPools();
|
||||
await ExecAsync(_master,
|
||||
$"ALTER DATABASE [{_dbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{_dbName}]").ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static async Task ExecAsync(string cs, string sql)
|
||||
{
|
||||
await using var c = new SqlConnection(cs);
|
||||
await c.OpenAsync().ConfigureAwait(false);
|
||||
await using var cmd = c.CreateCommand();
|
||||
cmd.CommandText = sql;
|
||||
await cmd.ExecuteNonQueryAsync().ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Shares one throwaway database across the integration tests.</summary>
|
||||
[CollectionDefinition(Name)]
|
||||
public sealed class SqlHistorianCollection : ICollectionFixture<SqlHistorianFixture>
|
||||
{
|
||||
/// <summary>Collection name.</summary>
|
||||
public const string Name = "SqlHistorian";
|
||||
}
|
||||
Reference in New Issue
Block a user