feat(localdb): rewire SiteEventLogger onto the consolidated site database

Completes Task 5a of the LocalDb Phase 1 adoption plan. The GUID-id half landed
with Task 2 (727fa48c) because leaving it out would have committed a writer that
could not satisfy site_events.id NOT NULL; this is the connection half, which
was blocked on Task 3.

SiteEventLogger owned a SqliteConnection built from SiteEventLogOptions
.DatabasePath. It now takes ILocalDb and gets that connection from
CreateConnection() - already open, pragma-configured, and carrying the
zb_hlc_next() UDF that site_events' capture triggers call. It still owns and
disposes the connection; WithConnection's signature and locking semantics are
untouched, so EventLogQueryService and EventLogPurgeService are unaffected.

The connectionStringOverride seam is deleted rather than preserved. site_events
is a replicated table, so a raw connection lacks the UDF and fails closed on
every insert - an override could only produce a logger that cannot write. It had
no callers outside this class (the connectionStringOverride hits elsewhere in the
tree belong to SqliteAuditWriter, a different type).

Tests: a shared TestLocalDb helper gives each fixture a real ILocalDb over a temp
file. Real rather than stubbed on purpose - a stub handing back a bare
SqliteConnection would pass today and fail closed the moment the table is
registered, which is exactly the silent-wiring failure mode this family has hit
three times. ServiceWiringTests' DI path also needed AddZbLocalDb, mirroring
SiteServiceRegistration.

Verified: build 0 warnings; SiteEventLogging 70/70, Host 294/294,
SiteRuntime 530/530.

Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
This commit is contained in:
Joseph Doherty
2026-07-19 09:07:49 -04:00
parent 0b5e9b44f6
commit 0d8a80aa27
9 changed files with 154 additions and 36 deletions
@@ -2,6 +2,7 @@ using System.Threading.Channels;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using ZB.MOM.WW.LocalDb;
namespace ZB.MOM.WW.ScadaBridge.SiteEventLogging;
@@ -40,29 +41,36 @@ public class SiteEventLogger : ISiteEventLogger, IDisposable
private bool _disposed;
/// <summary>
/// Initializes the event logger, opens the SQLite connection, and starts the background writer loop.
/// Initializes the event logger over the consolidated site database and starts the
/// background writer loop.
/// </summary>
/// <param name="options">Site event log configuration (database path, retention settings).</param>
/// <param name="options">Site event log configuration (retention settings, queue capacity).</param>
/// <param name="logger">Logger for write-failure diagnostics.</param>
/// <param name="connectionStringOverride">Optional connection string override; uses the configured path when null.</param>
/// <param name="localDb">
/// The consolidated site database. The connection it hands out is already open and
/// carries the <c>zb_hlc_next()</c> UDF that <c>site_events</c>' capture triggers
/// call. This logger owns the connection for its lifetime and disposes it.
/// </param>
/// <remarks>
/// The former <c>connectionStringOverride</c> seam is deliberately gone rather than
/// preserved. <c>site_events</c> is a replicated table, so a raw connection lacking
/// the UDF would fail closed on every insert — an override would only let a caller
/// build a logger that cannot write. <c>SiteEventLogOptions.DatabasePath</c> is
/// likewise no longer read: the location is <c>LocalDb:Path</c>.
/// </remarks>
public SiteEventLogger(
IOptions<SiteEventLogOptions> options,
ILogger<SiteEventLogger> logger,
string? connectionStringOverride = null)
ILocalDb localDb)
{
ArgumentNullException.ThrowIfNull(options);
ArgumentNullException.ThrowIfNull(localDb);
_logger = logger;
// Cache=Shared is a cross-connection optimisation
// that lets multiple SqliteConnections share an in-process page cache.
// This logger owns exactly one SqliteConnection and serialises all
// access through _writeLock, so the mode is dormant — at best dead
// configuration, at worst a small future foot-gun for any second
// connection opened to the same file. A test path that genuinely
// needs Cache=Shared can still inject it via connectionStringOverride.
var connectionString = connectionStringOverride
?? $"Data Source={options.Value.DatabasePath}";
_connection = new SqliteConnection(connectionString);
_connection.Open();
// Already open — CreateConnection returns a live, pragma-configured,
// UDF-registered connection. Calling Open() on it again would throw.
_connection = localDb.CreateConnection();
InitializeSchema();