using Microsoft.Extensions.Configuration; using ZB.MOM.WW.LocalDb; using ZB.MOM.WW.ScadaBridge.SiteEventLogging; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Persistence; using ZB.MOM.WW.ScadaBridge.SiteRuntime.Tracking; using ZB.MOM.WW.ScadaBridge.StoreAndForward; namespace ZB.MOM.WW.ScadaBridge.Host; /// /// Initializes the consolidated site database — the single ZB.MOM.WW.LocalDb-managed /// file that holds the site node's replicated state. /// /// /// /// Ordering is load-bearing. Table DDL must run BEFORE /// RegisterReplicated, and every row that should replicate must be written /// AFTER it. Capture is trigger-based change-data-capture: the library neither /// captures nor snapshots rows that already existed when the triggers were installed, /// so a row written before registration is invisible to the peer forever — silently, /// with no error anywhere. /// /// /// Runs inside the AddZbLocalDb onReady callback, once, before any caller /// receives the singleton. onReady is a synchronous /// Action<ILocalDb>, hence the direct CreateConnection() rather than /// blocking on the async execute API. A throw here propagates out of the first /// GetRequiredService<ILocalDb>() and fails host startup, which is the /// intent: a site node that cannot establish its schema must not come up half-working. /// /// public static class SiteLocalDbSetup { /// /// Creates the site node's replicated tables, opts them into change capture, and /// migrates any pre-Phase-1 databases in. /// /// The LocalDb instance being initialized. /// Configuration, for the legacy migrator's old path keys and node name. public static void OnReady(ILocalDb db, IConfiguration config) { ArgumentNullException.ThrowIfNull(db); ArgumentNullException.ThrowIfNull(config); using (var connection = db.CreateConnection()) { OperationTrackingSchema.Apply(connection); SiteEventLogSchema.Apply(connection); // Phase 2: the site's configuration tables and the store-and-forward buffer // now live in this file too. SiteStorageSchema.Apply(connection); StoreAndForwardSchema.Apply(connection); } // Both tables qualify: each has an explicit primary key (RegisterReplicated // rejects tables without one) and no BLOB columns (which json_object cannot // capture). Registration is idempotent and installs the capture triggers. db.RegisterReplicated("OperationTracking"); db.RegisterReplicated("site_events"); // Phase 2: the store-and-forward buffer and the seven site configuration tables. // These replaced the bespoke SiteReplicationActor and StoreAndForward // ReplicationService, which shipped hand-written Add/Remove/Park/Requeue operations // over Akka; both were deleted in the same commit that added these lines, so the // two mechanisms never ran at once. // // Both composite-PK tables are fine: RegisterReplicated orders multi-column PKs by // ordinal. No Phase 2 table has a BLOB column, which it would reject. db.RegisterReplicated("sf_messages"); db.RegisterReplicated("deployed_configurations"); db.RegisterReplicated("static_attribute_overrides"); db.RegisterReplicated("shared_scripts"); db.RegisterReplicated("external_systems"); db.RegisterReplicated("database_connections"); db.RegisterReplicated("data_connection_definitions"); db.RegisterReplicated("native_alarm_state"); // notification_lists and smtp_configurations are created but deliberately NOT // registered. They are permanently empty by design — the site-side write paths were // removed on 2026-07-10, the legacy migrator skips them, and the active node's // artifact apply purges them on every deploy. Registering them would open a standing // replication channel whose only historical payload was plaintext SMTP passwords, in // exchange for replicating nothing. Anyone adding them here should first establish // that a site has a legitimate reason to hold SMTP credentials at all. // AFTER registration, so migrated rows enter the oplog and reach the peer like // any other write. Before it, they would be invisible to replication forever. SiteLocalDbLegacyMigrator.Migrate(db, config); } }