using System.Data;
using Microsoft.Data.SqlClient;
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer;
///
/// Connection + command helpers shared by every SQL historian component.
/// Usage contract for writers/readers: call
/// once (it is cheap after the first success), then . Build SQL with
/// for every object name and bind every timestamp through or a TVP built by
/// — never a DataTable (it infers 3.33 ms datetime).
///
internal sealed class SqlHistorianConnection
{
private static readonly int[] TransientNumbers =
[1205, -2, 40613, 40197, 40501, 49918, 4060, 233, 10053, 10054, 10060, 11001];
private readonly string _connectionString;
/// Creates the helper; options must already be valid.
/// Validated options.
public SqlHistorianConnection(SqlServerHistorianOptions options)
{
ArgumentNullException.ThrowIfNull(options);
if (!SqlServerHistorianOptions.IsValidSchemaName(options.Schema))
throw new ArgumentException("Invalid schema name.", nameof(options));
_connectionString = options.ConnectionString;
Schema = options.Schema;
CommandTimeoutSeconds = Math.Max(1, (int)Math.Ceiling(options.CommandTimeout.TotalSeconds));
}
/// The validated schema name.
public string Schema { get; }
/// Command timeout applied by , in seconds.
public int CommandTimeoutSeconds { get; }
/// Schema-qualifies an object name: [schema].[obj].
/// Unquoted object name (a fixed literal from this project, never user input).
/// The quoted two-part name.
public string Q(string obj) => $"[{Schema}].[{obj}]";
/// Opens a new connection (no bootstrap; see the class remarks).
/// Cancellation.
/// An open connection the caller disposes.
public async Task OpenAsync(CancellationToken ct)
{
var conn = new SqlConnection(_connectionString);
try
{
await conn.OpenAsync(ct).ConfigureAwait(false);
return conn;
}
catch
{
await conn.DisposeAsync().ConfigureAwait(false);
throw;
}
}
/// Creates a text command with the configured timeout.
/// Open connection.
/// Command text.
/// Optional transaction.
/// The command.
public SqlCommand CreateCommand(SqlConnection conn, string sql, SqlTransaction? tx = null)
{
var cmd = conn.CreateCommand();
cmd.CommandText = sql;
cmd.CommandTimeout = CommandTimeoutSeconds;
cmd.Transaction = tx;
return cmd;
}
/// Adds a datetime2(7) scalar parameter (full 100 ns tick precision, UTC kind).
/// Target command.
/// Parameter name including @.
/// Timestamp; treated as UTC.
/// The added parameter.
public static SqlParameter AddDateTime2(SqlCommand cmd, string name, DateTime value)
{
var p = new SqlParameter(name, SqlDbType.DateTime2)
{
Scale = 7,
Value = DateTime.SpecifyKind(value, DateTimeKind.Utc),
};
cmd.Parameters.Add(p);
return p;
}
/// Adds a table-valued parameter of one of this schema's table types.
/// Target command.
/// Parameter name including @.
/// Unqualified type name (SampleRow or TimeRow).
/// Non-empty SqlDataRecord sequence (SqlClient rejects an empty one — skip the call).
/// The added parameter.
public SqlParameter AddStructured(SqlCommand cmd, string name, string typeName,
IEnumerable rows)
{
var p = new SqlParameter(name, SqlDbType.Structured) { TypeName = Q(typeName), Value = rows };
cmd.Parameters.Add(p);
return p;
}
/// Reads a datetime2 column as a UTC (the driver returns Unspecified).
/// Reader positioned on a row.
/// Column ordinal.
/// UTC timestamp.
public static DateTime GetUtc(SqlDataReader reader, int ordinal) =>
DateTime.SpecifyKind(reader.GetDateTime(ordinal), DateTimeKind.Utc);
/// True for connection/timeout/deadlock/throttling errors worth retrying.
/// Exception from a SQL call.
/// Whether a retry may succeed.
public static bool IsTransient(Exception ex)
{
if (ex is TimeoutException) return true;
if (ex is not SqlException sql) return false;
if (sql.IsTransient) return true;
foreach (SqlError e in sql.Errors)
if (Array.IndexOf(TransientNumbers, e.Number) >= 0) return true;
return Array.IndexOf(TransientNumbers, sql.Number) >= 0;
}
/// True when is a unique-key violation (2627/2601): "already present".
/// Exception from a SQL call.
/// Whether it is a duplicate-key error.
public static bool IsDuplicateKey(Exception ex) =>
ex is SqlException sql && sql.Errors.Cast().Any(e => e.Number is 2627 or 2601);
}