diff --git a/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/Internal/LocalDbSchema.cs b/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/Internal/LocalDbSchema.cs index b0b5528..a704ab1 100644 --- a/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/Internal/LocalDbSchema.cs +++ b/ZB.MOM.WW.LocalDb/src/ZB.MOM.WW.LocalDb/Internal/LocalDbSchema.cs @@ -14,6 +14,8 @@ internal static class LocalDbSchema hlc_high_water INTEGER NOT NULL DEFAULT 0, schema_version INTEGER NOT NULL ); + -- AUTOINCREMENT: seq must never be reused after row pruning, or peer watermarks/acks would + -- silently skip/misattribute rows sharing a reused seq value. CREATE TABLE IF NOT EXISTS __localdb_oplog ( seq INTEGER PRIMARY KEY AUTOINCREMENT, table_name TEXT NOT NULL, @@ -91,7 +93,9 @@ internal static class LocalDbSchema using var cmd = connection.CreateCommand(); cmd.CommandText = "SELECT node_id, hlc_high_water FROM __localdb_meta WHERE id = 1"; using var reader = cmd.ExecuteReader(); - reader.Read(); + if (!reader.Read()) + throw new InvalidOperationException( + "LocalDb schema not initialized: __localdb_meta is empty. Call EnsureCreated first."); return (reader.GetString(0), reader.GetInt64(1)); } diff --git a/ZB.MOM.WW.LocalDb/tests/ZB.MOM.WW.LocalDb.Tests/HybridLogicalClockTests.cs b/ZB.MOM.WW.LocalDb/tests/ZB.MOM.WW.LocalDb.Tests/HybridLogicalClockTests.cs index d16a6e3..5ea98fa 100644 --- a/ZB.MOM.WW.LocalDb/tests/ZB.MOM.WW.LocalDb.Tests/HybridLogicalClockTests.cs +++ b/ZB.MOM.WW.LocalDb/tests/ZB.MOM.WW.LocalDb.Tests/HybridLogicalClockTests.cs @@ -1,3 +1,4 @@ +using System.Collections.Concurrent; using ZB.MOM.WW.LocalDb.Hlc; namespace ZB.MOM.WW.LocalDb.Tests; @@ -87,4 +88,20 @@ public sealed class HybridLogicalClockTests Assert.Equal(ms + 1, HybridLogicalClock.PhysicalMs(next)); Assert.Equal(0, HybridLogicalClock.Counter(next)); } + + [Fact] + public void Next_ConcurrentCallers_NoDuplicates_StrictlyIncreasing() + { + var clock = new HybridLogicalClock(); + var results = new ConcurrentBag(); + + Parallel.For(0, 8, _ => + { + for (var i = 0; i < 1000; i++) + results.Add(clock.Next()); + }); + + Assert.Equal(8000, results.Count); + Assert.Equal(8000, results.Distinct().Count()); + } } diff --git a/ZB.MOM.WW.LocalDb/tests/ZB.MOM.WW.LocalDb.Tests/LocalDbSchemaTests.cs b/ZB.MOM.WW.LocalDb/tests/ZB.MOM.WW.LocalDb.Tests/LocalDbSchemaTests.cs index 55598e8..cef4ec3 100644 --- a/ZB.MOM.WW.LocalDb/tests/ZB.MOM.WW.LocalDb.Tests/LocalDbSchemaTests.cs +++ b/ZB.MOM.WW.LocalDb/tests/ZB.MOM.WW.LocalDb.Tests/LocalDbSchemaTests.cs @@ -54,6 +54,9 @@ public sealed class LocalDbSchemaTests : IDisposable "__localdb_row_version", "__localdb_applying", "__localdb_dead_letter" }) Assert.Contains(expected, tables); + + Assert.Equal(1, ScalarLong(conn, + "SELECT count(*) FROM sqlite_master WHERE type='index' AND name='__localdb_oplog_hlc'")); } [Fact]