diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/SiteLocalDbSetup.cs b/src/ZB.MOM.WW.ScadaBridge.Host/SiteLocalDbSetup.cs index 13b08ae4..626c6d5d 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/SiteLocalDbSetup.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/SiteLocalDbSetup.cs @@ -1,7 +1,9 @@ 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; @@ -44,6 +46,12 @@ public static class SiteLocalDbSetup { 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. Created here, deliberately NOT registered — see + // below. + SiteStorageSchema.Apply(connection); + StoreAndForwardSchema.Apply(connection); } // Both tables qualify: each has an explicit primary key (RegisterReplicated @@ -52,6 +60,13 @@ public static class SiteLocalDbSetup db.RegisterReplicated("OperationTracking"); db.RegisterReplicated("site_events"); + // The Phase 2 tables are created above but NOT registered yet. The bespoke + // SiteReplicationActor / StoreAndForward ReplicationService still own replicating + // them until the Task 14 cutover deletes both and registers these in one commit. + // Registering them now would mean two independent replicators writing the same + // rows — harmless in principle (both upsert) but it would mask a defect in either + // one, which is exactly what the cutover needs to be able to see. + // 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); diff --git a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/SiteLocalDbWiringTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/SiteLocalDbWiringTests.cs index c2d3fc91..f9e8e3eb 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/SiteLocalDbWiringTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/SiteLocalDbWiringTests.cs @@ -215,6 +215,62 @@ public class SiteLocalDbWiringTests : IDisposable Assert.Contains(LocalDbMetrics.MeterName, SiteServiceRegistration.ObservedMeters); } + [Fact] + public void Site_LocalDb_CreatesThePhase2Tables_InTheConsolidatedFile() + { + // Phase 2 (Task 7). The nine configuration tables and the store-and-forward buffer + // now live in the consolidated file rather than in scadabridge.db and + // store-and-forward.db. Asserting through the REAL composition root is what proves + // OnReady applies both schemas — the stores themselves also call Apply, so a test + // that went through a store would pass even if OnReady never touched them. + var db = _host.Services.GetRequiredService(); + + var tables = TableNames(db); + + // One from each Phase 1 schema, then every Phase 2 table. + Assert.Contains("OperationTracking", tables); + Assert.Contains("site_events", tables); + Assert.Contains("sf_messages", tables); + foreach (var table in new[] + { + "deployed_configurations", "static_attribute_overrides", "shared_scripts", + "external_systems", "database_connections", "notification_lists", + "data_connection_definitions", "smtp_configurations", "native_alarm_state", + }) + { + Assert.Contains(table, tables); + } + } + + [Fact] + public void Site_LocalDb_DoesNotYetRegisterThePhase2Tables() + { + // The "not yet" is the assertion that matters, and it is why this is an EXACTLY + // check rather than a Contains. Until the Task 14 cutover, the bespoke + // SiteReplicationActor and the StoreAndForward ReplicationService still own these + // tables. Registering them early would run two replicators over the same rows and + // hide a defect in either one behind the other's writes. + // + // When Task 14 lands, this test does not get deleted — it gets inverted. + var db = _host.Services.GetRequiredService(); + + Assert.Equal( + ["OperationTracking", "site_events"], + db.ReplicatedTables.Keys.OrderBy(k => k, StringComparer.Ordinal).ToArray()); + } + + private static HashSet TableNames(ILocalDb db) + { + using var connection = db.CreateConnection(); + using var cmd = connection.CreateCommand(); + cmd.CommandText = "SELECT name FROM sqlite_master WHERE type = 'table'"; + using var reader = cmd.ExecuteReader(); + + var names = new HashSet(StringComparer.Ordinal); + while (reader.Read()) names.Add(reader.GetString(0)); + return names; + } + [Fact] public void Site_LocalDb_CreatesTheConfiguredFile() {