using Microsoft.Data.Sqlite; namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging; /// /// DDL for the site_events table, extracted from so /// it can be applied by whoever owns the database file. /// /// Under LocalDb Phase 1 that owner is the Host's AddZbLocalDb onReady callback: /// the table must exist before ILocalDb.RegisterReplicated installs its capture /// triggers, and rows written before registration are never captured or snapshotted. /// /// /// Schema change. The primary key is a TEXT GUID minted by the writer, replacing /// the historical INTEGER PRIMARY KEY AUTOINCREMENT. This is required, not /// cosmetic: replication resolves conflicts by last-writer-wins on the primary key, so /// two site nodes each minting autoincrement id 1 for unrelated events would be treated /// as one row and silently overwrite each other. GUID keys make the event log a pure /// union across the pair. Consumers that relied on the id being monotonic — the keyset /// paging cursor and the storage-cap purge — order by timestamp instead. /// /// /// Deliberately depends only on Microsoft.Data.Sqlite, not on the LocalDb /// library — the Host needs to apply this DDL to a LocalDb-managed connection, but /// nothing about the schema itself is LocalDb-specific. /// /// public static class SiteEventLogSchema { /// /// Creates the site_events table and its indexes when absent. Idempotent — /// safe to run on every startup. /// /// An open connection to the database that should hold the table. public static void Apply(SqliteConnection connection) { ArgumentNullException.ThrowIfNull(connection); // arch-review 2026-08-19 P4-6: this method used to also set // "PRAGMA auto_vacuum = INCREMENTAL" here, on the theory that setting it right // before CREATE TABLE would make it take effect on a fresh database. That was // wrong in every real call path: SQLite only honors an auto_vacuum change on a // database that has NO tables yet, and by the time this method runs — via the // Host's AddZbLocalDb onReady callback (SiteLocalDbSetup.OnReady) OR via // SiteEventLogger's own InitializeSchema fallback for directly-constructed // instances — the consolidated file already holds LocalDb's own bookkeeping // tables (SqliteLocalDb's constructor runs LocalDbSchema.EnsureCreated on its // master connection before either caller ever gets a connection). So the pragma // here had never once taken effect; it was a silent, permanent no-op. The real // fix now lives in Host/SiteLocalDbSetup.cs // (EnsureAutoVacuumOnFreshFile), which pre-creates a genuinely fresh LocalDb file // with the pragma set BEFORE AddZbLocalDb's lazy singleton ever constructs // SqliteLocalDb — and, being a Host-layer, one-time, pre-file-existence step, it // helps ONLY brand-new site databases; every file created before this fix shipped // stays auto_vacuum=NONE forever (see the PurgeByStorageCap remarks in // EventLogPurgeService for the operational consequence). using var cmd = connection.CreateCommand(); cmd.CommandText = """ CREATE TABLE IF NOT EXISTS site_events ( id TEXT NOT NULL PRIMARY KEY, timestamp TEXT NOT NULL, event_type TEXT NOT NULL, severity TEXT NOT NULL, instance_id TEXT, source TEXT NOT NULL, message TEXT NOT NULL, details TEXT ); CREATE INDEX IF NOT EXISTS idx_events_timestamp ON site_events(timestamp); CREATE INDEX IF NOT EXISTS idx_events_type ON site_events(event_type); CREATE INDEX IF NOT EXISTS idx_events_instance ON site_events(instance_id); CREATE INDEX IF NOT EXISTS idx_events_severity ON site_events(severity); """; // The query service also supports keyword search via leading-wildcard // LIKE on message/source. A leading-wildcard LIKE cannot use a B-tree // index, so that path intentionally full-scans; severity/event_type/ // instance_id/timestamp filters above are all covered. cmd.ExecuteNonQuery(); } }