From 51c54f84ac29b8cf143d5cf94246f3f02f27d04b Mon Sep 17 00:00:00 2001 From: Jim Borden Date: Wed, 20 Aug 2025 13:35:15 +0900 Subject: [PATCH] CBL-7277 CBL-7281 Enable version vector functionality and tests, and change Timestamp to ulong instead of DateTimeOffset Also add in an extension to create a DateTimeOffset from the ulong --- .../API/Database/Database.cs | 2 +- .../API/Document/Document.cs | 54 +++++++++++-------- .../VersionVectorTests.cs | 18 +++---- 3 files changed, 43 insertions(+), 31 deletions(-) diff --git a/src/Couchbase.Lite.Shared/API/Database/Database.cs b/src/Couchbase.Lite.Shared/API/Database/Database.cs index e8317ba57..56e8ea536 100644 --- a/src/Couchbase.Lite.Shared/API/Database/Database.cs +++ b/src/Couchbase.Lite.Shared/API/Database/Database.cs @@ -107,7 +107,7 @@ public sealed unsafe partial class Database : IChangeObservable + /// An extension class for helping to turn a nanosecond based timestamp into a + /// object + /// + public static class TimestampExtensions + { + private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc); + + /// + /// Converts the nanosecond timestamp to a DateTimeOffset in UTC time + /// + /// The nanosecond timestamp + /// The DateTimeOffset object using the timestamp, or null if it was invalid + public static DateTimeOffset? AsDateTimeOffset(this ulong rawVal) + { + if(rawVal == 0) { + return null; + } + + // .NET ticks are in 100 nanosecond intervals + return UnixEpoch + TimeSpan.FromTicks((long)(rawVal / 100)); + } + } /// /// A class representing a document which cannot be altered /// @@ -41,8 +64,6 @@ public unsafe class Document : IDictionaryObject, IJSON, IDisposable { private const string Tag = nameof(Document); - private static readonly DateTimeOffset UnixEpoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); - #region Variables private string? _revId; @@ -65,16 +86,16 @@ public unsafe class Document : IDictionaryObject, IJSON, IDisposable internal C4DatabaseWrapper c4Db { get { - Debug.Assert(Database != null && Database.c4db != null); - return Database!.c4db!; + Debug.Assert(Database is { c4db: not null }); + return Database.c4db; } } internal C4CollectionWrapper c4Coll { get { - Debug.Assert(Collection != null && Collection.c4coll != null); - return Collection!.c4coll!; + Debug.Assert(Collection != null); + return Collection!.c4coll; } } @@ -164,25 +185,16 @@ public string? RevisionID } /// - /// The hybrid logical timestamp that the revision was created. + /// The hybrid logical timestamp that the revision was created, represented in nanoseconds + /// from the unix epoch. If you want this value as a DateTimeOffset you can use the + /// convenience function AsDateTimeOffset. + /// Just be aware that DateTimeOffset only handles 100 nanosecond resolution. /// - public DateTimeOffset? Timestamp + public ulong Timestamp { get { using var scope = ThreadSafety.BeginLockedScope(); - var rawVal = c4Doc?.HasValue == true ? NativeRaw.c4rev_getTimestamp(c4Doc.RawDoc->selectedRev.revID) : 0; - if(rawVal == 0) { - return null; - } - - // .NET ticks are in 100 nanosecond intervals - rawVal /= 100; - - if(rawVal > Int64.MaxValue) { - throw new OverflowException("The returned value from LiteCore is too large to be represented by DateTimeOffset"); - } - - return UnixEpoch + TimeSpan.FromTicks((long)rawVal); + return c4Doc?.HasValue == true ? NativeRaw.c4rev_getTimestamp(c4Doc.RawDoc->selectedRev.revID) : 0; } } diff --git a/src/Couchbase.Lite.Tests.Shared/VersionVectorTests.cs b/src/Couchbase.Lite.Tests.Shared/VersionVectorTests.cs index c7400b96b..c9921692c 100644 --- a/src/Couchbase.Lite.Tests.Shared/VersionVectorTests.cs +++ b/src/Couchbase.Lite.Tests.Shared/VersionVectorTests.cs @@ -48,16 +48,16 @@ public VersionVectorTest(ITestOutputHelper output) : base(output) /// 5. Get the document id = "doc1" from the database. /// 6. Get document's timestamp and check that the timestamp is the same as the timestamp from step 4. /// - [Fact(Skip = "Version vectors not turned on yet")] + [Fact] public void TestDocumentTimestamp() { using var doc = new MutableDocument("doc1"); - doc.Timestamp.ShouldBeNull("because the doc has not been saved yet"); + doc.Timestamp.ShouldBe(0UL, "because the doc has not been saved yet"); DefaultCollection.Save(doc); - doc.Timestamp.ShouldNotBeNull("because the doc is now saved"); + doc.Timestamp.ShouldNotBe(0UL, "because the doc is now saved"); using var savedDoc = DefaultCollection.GetDocument("doc1"); savedDoc.ShouldNotBeNull("because the document was just saved"); - savedDoc!.Timestamp.ShouldBe(doc.Timestamp, "because the timestamp should not change just from a read"); + savedDoc.Timestamp.ShouldBe(doc.Timestamp, "because the timestamp should not change just from a read"); } /// @@ -72,7 +72,7 @@ public void TestDocumentTimestamp() /// 5. Get the document id = "doc1" from the database. /// 6. Get document's _revisionIDs and check that the value returned is not null /// - [Fact(Skip = "Version vectors not turned on yet")] + [Fact] public void TestDocumentRevisionHistory() { using var doc = new MutableDocument("doc1"); @@ -82,7 +82,7 @@ public void TestDocumentRevisionHistory() using var savedDoc = DefaultCollection.GetDocument("doc1"); savedDoc.ShouldNotBeNull("because the document was just saved"); - savedDoc!.RevisionIDs().ShouldNotBeNull("because the saved document should contain at least one revision ID"); + savedDoc.RevisionIDs().ShouldNotBeNull("because the saved document should contain at least one revision ID"); } public enum DefaultConflictLWWMode @@ -110,7 +110,7 @@ public enum DefaultConflictLWWMode /// 6.Start a single shot pull replicator to pull documents from "db2" to "db1". /// 7. Get the document "doc2" from "db1" and check that the content is {"key": "value2"}. /// - [Theory(Skip = "Version vectors not turned on yet")] + [Theory] [InlineData(DefaultConflictLWWMode.SaveDB2First)] [InlineData(DefaultConflictLWWMode.SaveDB1First)] public void TestDefaultConflictResolver(DefaultConflictLWWMode lwwMode) @@ -148,7 +148,7 @@ public void TestDefaultConflictResolver(DefaultConflictLWWMode lwwMode) using var doc = db1.GetDefaultCollection().GetDocument("doc1"); doc.ShouldNotBeNull("because it was just saved"); - doc!["key"].Value.ShouldBe(expectedValue, "because otherwise the conflict resolver behaved unexpectedly"); + doc["key"].Value.ShouldBe(expectedValue, "because otherwise the conflict resolver behaved unexpectedly"); } /// @@ -168,7 +168,7 @@ public void TestDefaultConflictResolver(DefaultConflictLWWMode lwwMode) /// 4.Start a single shot pull replicator to pull documents from "db2" to "db1". /// 5. Get the document "doc1" from "db1" and check that the returned document is null. /// - [Fact(Skip = "Version vectors not turned on yet")] + [Fact] public void TestDefaultConflictResolverDeleteWins() { Database.Delete("db1", null);