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); }