namespace ZB.MOM.WW.Secrets.Replicator.SqlServer;
///
/// Schema constants and DDL for the shared SQL-Server secret store. Mirrors
/// ZB.MOM.WW.Secrets.Sqlite.SqliteSecretsSchema column-for-column so a row means exactly the
/// same thing in either store.
///
///
///
/// Timestamps are ISO-8601 nvarchar, not datetimeoffset. This looks wrong for a
/// SQL-Server schema and is deliberate: the same
/// rows replicate between this store and SQLite (which stores "O"-format text), and
/// last-writer-wins compares those timestamps for equality. datetimeoffset rounds to 100ns
/// while .NET DateTimeOffset ticks are also 100ns but the round-trip through the driver is
/// not guaranteed bit-identical across providers — a sub-tick difference would turn a tie into a
/// spurious "newer", and two nodes would then overwrite each other forever. Storing the exact
/// round-trippable text both providers already agree on removes the failure mode entirely.
///
///
/// The cost is that ORDER BY updated_utc is a lexicographic sort. That is still chronological
/// for "O"-format UTC strings (fixed-width, zero-padded, always +00:00), which is what
/// the store writes.
///
///
public static class SqlServerSecretsSchema
{
///
/// The schema version this build creates and supports. The migrator refuses a database whose
/// recorded version is newer than this.
///
public const int CurrentVersion = 1;
/// Default schema the secret tables live in.
public const string DefaultSchemaName = "zbsecrets";
/// Name of the single-row table tracking the applied schema version.
public const string SchemaVersionTable = "schema_version";
/// Name of the table storing envelope-encrypted secret records.
public const string SecretTable = "secret";
///
/// Returns the idempotent DDL that provisions the schema, both tables, and the supporting index
/// in .
///
/// The already-validated SQL schema name to create the objects in.
/// The DDL batch.
///
/// is interpolated, not parameterized — T-SQL has no parameter form
/// for an object name. It is re-validated here against the same strict identifier allow-list
/// enforces, and bracket-quoted below. The
/// duplicate check is deliberate: this method is public, so it must defend itself rather
/// than trust that every caller routed through the options type.
///
/// is not a plain identifier.
public static string CreateSchemaDdl(string schemaName)
{
SqlServerSecretsOptions.ValidateIdentifier(schemaName);
return $"""
IF NOT EXISTS (SELECT 1 FROM sys.schemas WHERE name = N'{schemaName}')
EXEC(N'CREATE SCHEMA [{schemaName}]');
IF OBJECT_ID(N'[{schemaName}].[{SchemaVersionTable}]', N'U') IS NULL
CREATE TABLE [{schemaName}].[{SchemaVersionTable}] (
id INT NOT NULL PRIMARY KEY CHECK (id = 1),
version INT NOT NULL,
applied_utc NVARCHAR(33) NOT NULL
);
IF OBJECT_ID(N'[{schemaName}].[{SecretTable}]', N'U') IS NULL
CREATE TABLE [{schemaName}].[{SecretTable}] (
name NVARCHAR(450) NOT NULL PRIMARY KEY,
description NVARCHAR(MAX) NULL,
content_type NVARCHAR(64) NOT NULL,
ciphertext VARBINARY(MAX) NOT NULL,
nonce VARBINARY(MAX) NOT NULL,
tag VARBINARY(MAX) NOT NULL,
wrapped_dek VARBINARY(MAX) NOT NULL,
wrap_nonce VARBINARY(MAX) NOT NULL,
wrap_tag VARBINARY(MAX) NOT NULL,
kek_id NVARCHAR(256) NOT NULL,
revision BIGINT NOT NULL CONSTRAINT DF_secret_revision DEFAULT 0,
is_deleted BIT NOT NULL CONSTRAINT DF_secret_is_deleted DEFAULT 0,
deleted_utc NVARCHAR(33) NULL,
created_utc NVARCHAR(33) NOT NULL,
updated_utc NVARCHAR(33) NOT NULL,
created_by NVARCHAR(256) NULL,
updated_by NVARCHAR(256) NULL
);
IF NOT EXISTS (
SELECT 1 FROM sys.indexes
WHERE name = N'ix_secret_updated_utc'
AND object_id = OBJECT_ID(N'[{schemaName}].[{SecretTable}]'))
CREATE INDEX ix_secret_updated_utc
ON [{schemaName}].[{SecretTable}] (updated_utc);
""";
}
}