feat(localdb): deployment-cache schema + OnReady registration

This commit is contained in:
Joseph Doherty
2026-07-20 10:28:39 -04:00
parent 44255fcb91
commit 3b65c24bfb
3 changed files with 246 additions and 0 deletions
@@ -0,0 +1,70 @@
using Microsoft.Data.Sqlite;
namespace ZB.MOM.WW.OtOpcUa.Runtime.Deployment;
/// <summary>
/// DDL for the node-local deployment-artifact cache: the chunked artifact table plus the
/// per-cluster current-deployment pointer.
/// </summary>
/// <remarks>
/// <para>
/// Deliberately depends on nothing but <see cref="SqliteConnection"/> so it can be applied
/// to any connection — the host's <c>LocalDbSetup.OnReady</c> in production, and a bare
/// connection in a test — without dragging the DI graph along.
/// </para>
/// <para>
/// <b>Why the artifact is chunked base64 TEXT and not one BLOB row.</b> Two independent
/// constraints force it. <c>RegisterReplicated</c> rejects BLOB columns outright; and the
/// replication engine batches by <i>row count</i> against gRPC's 4 MB default message cap,
/// so a single multi-megabyte artifact row would simply never be deliverable — at any
/// batch size. A 128 KiB raw chunk is ≈ 171 KB once base64-encoded, which keeps a
/// worst-case batch comfortably inside the cap.
/// </para>
/// <para>
/// <b>Why no autoincrement PKs.</b> Convergence is last-writer-wins keyed on the primary
/// key. Two nodes independently allocating rowid 7 for different rows would silently
/// overwrite one another. Every key here is TEXT or a composite of caller-supplied values.
/// </para>
/// </remarks>
public static class DeploymentCacheSchema
{
/// <summary>Table holding the artifact bytes, split into base64 chunks.</summary>
public const string ArtifactsTable = "deployment_artifacts";
/// <summary>Table holding one current-deployment pointer per cluster.</summary>
public const string PointerTable = "deployment_pointer";
/// <summary>
/// Creates both cache tables if they do not already exist. Idempotent.
/// </summary>
/// <param name="connection">
/// An already-open connection. <c>ILocalDb.CreateConnection()</c> hands out open,
/// pragma-configured connections — do not call <c>Open()</c> on one.
/// </param>
public static void Apply(SqliteConnection connection)
{
ArgumentNullException.ThrowIfNull(connection);
using var cmd = connection.CreateCommand();
cmd.CommandText = """
CREATE TABLE IF NOT EXISTS deployment_artifacts (
deployment_id TEXT NOT NULL,
chunk_index INTEGER NOT NULL,
cluster_id TEXT NOT NULL,
revision_hash TEXT NOT NULL,
chunk_count INTEGER NOT NULL,
chunk_base64 TEXT NOT NULL,
cached_at_utc TEXT NOT NULL,
PRIMARY KEY (deployment_id, chunk_index)
);
CREATE TABLE IF NOT EXISTS deployment_pointer (
cluster_id TEXT NOT NULL PRIMARY KEY,
deployment_id TEXT NOT NULL,
revision_hash TEXT NOT NULL,
artifact_sha256 TEXT NOT NULL,
applied_at_utc TEXT NOT NULL
);
""";
cmd.ExecuteNonQuery();
}
}