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