0658d244d0
- HistoryUpdate items with SourceTimestamp > now + ServerHistorian:HistoryUpdateMaxFutureSkew (default 1 day, <=0 disables) => BadOutOfRange in the node manager (every backend, never sent); clock injected via OtOpcUaNodeManager.Clock (host TimeProvider). - HistoryUpdate is Primary-only: off-Primary => BadInvalidState per node, after authorization, same IDriverPrimaryRoleSource verdict as the native-ack gate; HistoryRead ungated. - SqlServer: base schema requirement back to v1; SupportsModifiedTrail (v>=2). On v1 with AutoCreateSchema=false: writes/reads/events/retention work, modified reads + HistoryUpdate => BadHistoryOperationUnsupported with nothing sent, retention skips SampleModified, one Warning per boot. - QuestDB integration test pinning wall-clock TTL (far-future row evicts only partitions older than the TTL by the wall clock), with a positive control. - Docs: Historian.md, Configuration.md, security.md, CLAUDE.md (incl. QuestDB retry duplicates, #674.4 anonymous-readable trail with ACL off, #674.5 not implemented). Co-Authored-By: Claude <noreply@anthropic.com>
71 lines
3.6 KiB
C#
71 lines
3.6 KiB
C#
using System.Text.RegularExpressions;
|
|
|
|
namespace ZB.MOM.WW.OtOpcUa.Driver.Historian.SqlServer;
|
|
|
|
/// <summary>
|
|
/// Options for the SQL Server historian backend (the <c>ServerHistorian:SqlServer</c> config block).
|
|
/// </summary>
|
|
public sealed partial class SqlServerHistorianOptions
|
|
{
|
|
/// <summary>Required. ADO.NET connection string for the historian database (never the ConfigDb).</summary>
|
|
public string ConnectionString { get; set; } = "";
|
|
|
|
/// <summary>SQL schema holding the historian objects. Spliced into DDL/DML, so it is regex-validated.</summary>
|
|
public string Schema { get; set; } = "hist";
|
|
|
|
/// <summary>Per-command timeout. Must be positive.</summary>
|
|
public TimeSpan CommandTimeout { get; set; } = TimeSpan.FromSeconds(30);
|
|
|
|
/// <summary>
|
|
/// <c>true</c> creates the schema and applies missing scripts on first use; <c>false</c> only verifies
|
|
/// that the base schema version (<c>SqlServerSchemaBootstrapper.RequiredVersion</c>, 1) exists and fails every
|
|
/// historian call with a clear error otherwise. A verified v1 schema (no Part 11 modification trail, script 002
|
|
/// not applied) is served with modified reads and HistoryUpdate refused <c>BadHistoryOperationUnsupported</c>
|
|
/// and one Warning per boot (Gitea #674).
|
|
/// </summary>
|
|
public bool AutoCreateSchema { get; set; } = true;
|
|
|
|
/// <summary>
|
|
/// Days of history to keep, measured on source time. <c>null</c> (default) keeps everything. When set,
|
|
/// a driver node deletes older <c>hist.Sample</c>/<c>hist.Event</c> rows every <see cref="PurgeInterval"/>.
|
|
/// </summary>
|
|
public int? RetentionDays { get; set; }
|
|
|
|
/// <summary>How often the retention purge runs. Must be positive.</summary>
|
|
public TimeSpan PurgeInterval { get; set; } = TimeSpan.FromHours(1);
|
|
|
|
/// <summary>
|
|
/// Rows per <c>DELETE TOP(n)</c> batch. Default 4000 stays under SQL Server's 5000-lock escalation
|
|
/// threshold so a purge never escalates to a table lock.
|
|
/// </summary>
|
|
public int PurgeBatchSize { get; set; } = 4000;
|
|
|
|
[GeneratedRegex("^[A-Za-z_][A-Za-z0-9_]{0,127}$")]
|
|
private static partial Regex IdentifierRegex();
|
|
|
|
/// <summary>True when <paramref name="schema"/> is a safe SQL identifier.</summary>
|
|
/// <param name="schema">Candidate schema name.</param>
|
|
/// <returns>Whether it matches <c>^[A-Za-z_][A-Za-z0-9_]{0,127}$</c>.</returns>
|
|
public static bool IsValidSchemaName(string? schema) => schema is not null && IdentifierRegex().IsMatch(schema);
|
|
|
|
/// <summary>Validates the options without touching SQL.</summary>
|
|
/// <returns>One message per problem; empty when valid.</returns>
|
|
public IReadOnlyList<string> Validate()
|
|
{
|
|
var errors = new List<string>();
|
|
if (string.IsNullOrWhiteSpace(ConnectionString))
|
|
errors.Add("ServerHistorian:SqlServer:ConnectionString is required.");
|
|
if (!IsValidSchemaName(Schema))
|
|
errors.Add("ServerHistorian:SqlServer:Schema must be a SQL identifier matching ^[A-Za-z_][A-Za-z0-9_]{0,127}$.");
|
|
if (CommandTimeout <= TimeSpan.Zero)
|
|
errors.Add("ServerHistorian:SqlServer:CommandTimeout must be greater than zero.");
|
|
if (RetentionDays is < 1)
|
|
errors.Add("ServerHistorian:SqlServer:RetentionDays must be at least 1 when set (omit it to keep history forever).");
|
|
if (PurgeInterval <= TimeSpan.Zero)
|
|
errors.Add("ServerHistorian:SqlServer:PurgeInterval must be greater than zero.");
|
|
if (PurgeBatchSize < 1)
|
|
errors.Add("ServerHistorian:SqlServer:PurgeBatchSize must be at least 1.");
|
|
return errors;
|
|
}
|
|
}
|