Skip to content

CBL-7277 CBL-7281: Enable version vector functionality and tests #1718

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Aug 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Couchbase.Lite.Shared/API/Database/Database.cs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public sealed unsafe partial class Database : IChangeObservable<DatabaseChangedE
#region Constants

private static readonly C4DatabaseConfig2 DBConfig = new C4DatabaseConfig2 {
flags = C4DatabaseFlags.Create | C4DatabaseFlags.AutoCompact
flags = C4DatabaseFlags.Create | C4DatabaseFlags.AutoCompact | C4DatabaseFlags.VersionVectors
};

private const string DBExtension = "cblite2";
Expand Down
54 changes: 33 additions & 21 deletions src/Couchbase.Lite.Shared/API/Document/Document.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,36 @@

namespace Couchbase.Lite
{
/// <summary>
/// An extension class for helping to turn a nanosecond based timestamp into a
/// <see cref="DateTimeOffset"/> object
/// </summary>
public static class TimestampExtensions
{
private static readonly DateTime UnixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

/// <summary>
/// Converts the nanosecond timestamp to a DateTimeOffset in UTC time
/// </summary>
/// <param name="rawVal">The nanosecond timestamp</param>
/// <returns>The DateTimeOffset object using the timestamp, or null if it was invalid</returns>
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));
}
}
/// <summary>
/// A class representing a document which cannot be altered
/// </summary>
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;
Expand All @@ -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;
}
}

Expand Down Expand Up @@ -164,25 +185,16 @@ public string? RevisionID
}

/// <summary>
/// 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 <see cref="TimestampExtensions.AsDateTimeOffset(ulong)">AsDateTimeOffset</see>.
/// Just be aware that DateTimeOffset only handles 100 nanosecond resolution.
/// </summary>
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;
}
}

Expand Down
18 changes: 9 additions & 9 deletions src/Couchbase.Lite.Tests.Shared/VersionVectorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
/// </summary>
[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");
}

/// <summary>
Expand All @@ -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
/// </summary>
[Fact(Skip = "Version vectors not turned on yet")]
[Fact]
public void TestDocumentRevisionHistory()
{
using var doc = new MutableDocument("doc1");
Expand All @@ -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
Expand Down Expand Up @@ -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"}.
/// </summary>
[Theory(Skip = "Version vectors not turned on yet")]
[Theory]
[InlineData(DefaultConflictLWWMode.SaveDB2First)]
[InlineData(DefaultConflictLWWMode.SaveDB1First)]
public void TestDefaultConflictResolver(DefaultConflictLWWMode lwwMode)
Expand Down Expand Up @@ -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");
}

/// <summary>
Expand All @@ -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.
/// </summary>
[Fact(Skip = "Version vectors not turned on yet")]
[Fact]
public void TestDefaultConflictResolverDeleteWins()
{
Database.Delete("db1", null);
Expand Down