From c3f85ae2b6f270912281ec69c3846070a2f7efe6 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 18 Sep 2025 10:31:52 +0200 Subject: [PATCH 01/15] POC Exposing SnapshotTime --- src/MongoDB.Driver/ClientSessionHandle.cs | 2 + src/MongoDB.Driver/ClientSessionOptions.cs | 9 +- .../Core/Bindings/CoreSession.cs | 1 + .../Core/Bindings/CoreSessionOptions.cs | 14 ++- .../Core/Operations/ReadConcernHelper.cs | 2 +- src/MongoDB.Driver/IClientSession.cs | 5 + src/MongoDB.Driver/MongoClient.cs | 2 + .../AtClusterTimeTests.cs | 112 ++++++++++++++++++ 8 files changed, 144 insertions(+), 3 deletions(-) create mode 100644 tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs diff --git a/src/MongoDB.Driver/ClientSessionHandle.cs b/src/MongoDB.Driver/ClientSessionHandle.cs index 144e6a94991..084c4db740b 100644 --- a/src/MongoDB.Driver/ClientSessionHandle.cs +++ b/src/MongoDB.Driver/ClientSessionHandle.cs @@ -89,6 +89,8 @@ public IServerSession ServerSession } } + public BsonTimestamp SnapshotTime => _coreSession.SnapshotTime; + /// public ICoreSessionHandle WrappedCoreSession => _coreSession; diff --git a/src/MongoDB.Driver/ClientSessionOptions.cs b/src/MongoDB.Driver/ClientSessionOptions.cs index f4b7f80ccac..e670b274116 100644 --- a/src/MongoDB.Driver/ClientSessionOptions.cs +++ b/src/MongoDB.Driver/ClientSessionOptions.cs @@ -14,6 +14,7 @@ */ using System; +using MongoDB.Bson; using MongoDB.Driver.Core.Bindings; namespace MongoDB.Driver @@ -46,6 +47,11 @@ public class ClientSessionOptions /// public bool Snapshot { get; set;} + /// + /// //TODO + /// + public BsonTimestamp SnapshotTime { get; set; } + // internal methods internal CoreSessionOptions ToCore(bool isImplicit = false) { @@ -55,7 +61,8 @@ internal CoreSessionOptions ToCore(bool isImplicit = false) isCausallyConsistent: isCausallyConsistent, isImplicit: isImplicit, isSnapshot: Snapshot, - defaultTransactionOptions: DefaultTransactionOptions); + defaultTransactionOptions: DefaultTransactionOptions, + snapshotTime: SnapshotTime); } } } diff --git a/src/MongoDB.Driver/Core/Bindings/CoreSession.cs b/src/MongoDB.Driver/Core/Bindings/CoreSession.cs index 954c639e244..d3366cdc188 100644 --- a/src/MongoDB.Driver/Core/Bindings/CoreSession.cs +++ b/src/MongoDB.Driver/Core/Bindings/CoreSession.cs @@ -71,6 +71,7 @@ private CoreSession( { _cluster = Ensure.IsNotNull(cluster, nameof(cluster)); _options = Ensure.IsNotNull(options, nameof(options)); + _snapshotTime = options.SnapshotTime; } // public properties diff --git a/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs b/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs index d5697312fcf..6c8e97948cf 100644 --- a/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs +++ b/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs @@ -13,8 +13,11 @@ * limitations under the License. */ +using MongoDB.Bson; + namespace MongoDB.Driver.Core.Bindings { + //TODO Why is this class public? /// /// Core session options. /// @@ -25,6 +28,7 @@ public class CoreSessionOptions private readonly bool _isCausallyConsistent; private readonly bool _isImplicit; private readonly bool _isSnapshot; + private readonly BsonTimestamp _snapshotTime; // constructors /// @@ -34,16 +38,19 @@ public class CoreSessionOptions /// if set to true this session is an implicit session. /// if set to true this session is a snapshot session. /// The default transaction options. + /// //TODO public CoreSessionOptions( bool isCausallyConsistent = false, bool isImplicit = false, TransactionOptions defaultTransactionOptions = null, - bool isSnapshot = false) + bool isSnapshot = false, + BsonTimestamp snapshotTime = null) { _isCausallyConsistent = isCausallyConsistent; _isImplicit = isImplicit; _isSnapshot = isSnapshot; _defaultTransactionOptions = defaultTransactionOptions; + _snapshotTime = snapshotTime; } // public properties @@ -78,5 +85,10 @@ public CoreSessionOptions( /// true if this session is a snapshot session; otherwise, false. /// public bool IsSnapshot => _isSnapshot; + + /// + /// //TODO + /// + public BsonTimestamp SnapshotTime => _snapshotTime; } } diff --git a/src/MongoDB.Driver/Core/Operations/ReadConcernHelper.cs b/src/MongoDB.Driver/Core/Operations/ReadConcernHelper.cs index 4938b357df4..914c1967735 100644 --- a/src/MongoDB.Driver/Core/Operations/ReadConcernHelper.cs +++ b/src/MongoDB.Driver/Core/Operations/ReadConcernHelper.cs @@ -40,7 +40,7 @@ public static BsonDocument GetReadConcernForSnapshotSession(ICoreSession session Feature.SnapshotReads.ThrowIfNotSupported(connectionDescription.MaxWireVersion); var readConcernDocument = ReadConcern.Snapshot.ToBsonDocument(); - if (session.SnapshotTime != null) + if (session.SnapshotTime != null) // For the first read within a snapshot session, SnapshotTime will be null { readConcernDocument.Add("atClusterTime", session.SnapshotTime); } diff --git a/src/MongoDB.Driver/IClientSession.cs b/src/MongoDB.Driver/IClientSession.cs index a0809fcae3c..26d46cd5967 100644 --- a/src/MongoDB.Driver/IClientSession.cs +++ b/src/MongoDB.Driver/IClientSession.cs @@ -83,6 +83,11 @@ public interface IClientSession : IDisposable /// IServerSession ServerSession { get; } + /// + /// //TODO + /// + BsonTimestamp SnapshotTime { get; } + /// /// Gets the wrapped core session (intended for internal use only). /// diff --git a/src/MongoDB.Driver/MongoClient.cs b/src/MongoDB.Driver/MongoClient.cs index 4b70bfdfd16..bac494e1b9a 100644 --- a/src/MongoDB.Driver/MongoClient.cs +++ b/src/MongoDB.Driver/MongoClient.cs @@ -627,6 +627,8 @@ private IClientSessionHandle StartSession(ClientSessionOptions options) throw new NotSupportedException("Combining both causal consistency and snapshot options is not supported."); } + //TODO Throw an exception if SnapshotTime is set and Snapshot is not true. + options ??= new ClientSessionOptions(); if (_settings.Timeout.HasValue && options.DefaultTransactionOptions?.Timeout == null) { diff --git a/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs b/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs new file mode 100644 index 00000000000..8ed9d197968 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs @@ -0,0 +1,112 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +using System.Collections.Generic; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Attributes; +using Xunit; + +namespace MongoDB.Driver.Tests; + +public class AtClusterTimeTests +{ + [Fact] + public void AtClusterTime_should_work() + { + const string collectionName = "atClusterTimeTests"; + const string databaseName = "testDb"; + + using var client = DriverTestConfiguration.Client; + var database = client.GetDatabase(databaseName); + database.DropCollection(collectionName); + var collection = database.GetCollection(collectionName); + + var obj1 = new TestObject { Name = "obj1" }; + collection.InsertOne(obj1); + + BsonTimestamp clusterTime1; + + var filterDefinition = Builders.Filter.Empty; + var sortDefinition = Builders.Sort.Ascending(o => o.Name); + + var sessionOptions1 = new ClientSessionOptions + { + Snapshot = true + }; + + using (var session1 = client.StartSession(sessionOptions1)) + { + var results = collection.Find(session1, filterDefinition).Sort(sortDefinition).ToList(); + AssertOneObj(results); + + clusterTime1 = session1.SnapshotTime; + Assert.NotEqual(null, clusterTime1); + } + + var obj2 = new TestObject { Name = "obj2" }; + collection.InsertOne(obj2); + + var sessionOptions2 = new ClientSessionOptions + { + Snapshot = true, + SnapshotTime = clusterTime1 + }; + + //Snapshot read session at clusterTime1 should not see obj2 + using (var session2 = client.StartSession(sessionOptions2)) + { + var results = collection.Find(session2, filterDefinition).Sort(sortDefinition).ToList(); + AssertOneObj(results); + + var clusterTime2 = session2.SnapshotTime; + Assert.Equal(clusterTime2, clusterTime1); + } + + var sessionOptions3 = new ClientSessionOptions + { + Snapshot = true, + }; + + //Snapshot read session without cluster time should see obj2 + using (var session3 = client.StartSession(sessionOptions3)) + { + var results = collection.Find(session3, filterDefinition).Sort(sortDefinition).ToList(); + AssertTwoObjs(results); + + var clusterTime3 = session3.WrappedCoreSession.SnapshotTime; + Assert.NotEqual(clusterTime3, clusterTime1); + } + + void AssertOneObj(List objs) + { + Assert.Equal(1, objs.Count); + Assert.Equal("obj1", objs[0].Name); + } + + void AssertTwoObjs(List objs) + { + Assert.Equal(2, objs.Count); + Assert.Equal("obj1", objs[0].Name); + Assert.Equal("obj2", objs[1].Name); + } + } + + private class TestObject + { + [BsonId] + public ObjectId Id { get; set; } + public string Name { get; set; } + } +} \ No newline at end of file From e30d62c0ddbfea9ed8561606f0ef9e665f266f43 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 18 Sep 2025 10:40:37 +0200 Subject: [PATCH 02/15] Small corrections --- src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs | 1 - src/MongoDB.Driver/Core/Operations/ReadConcernHelper.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs b/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs index 6c8e97948cf..828ba94f1a4 100644 --- a/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs +++ b/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs @@ -17,7 +17,6 @@ namespace MongoDB.Driver.Core.Bindings { - //TODO Why is this class public? /// /// Core session options. /// diff --git a/src/MongoDB.Driver/Core/Operations/ReadConcernHelper.cs b/src/MongoDB.Driver/Core/Operations/ReadConcernHelper.cs index 914c1967735..4938b357df4 100644 --- a/src/MongoDB.Driver/Core/Operations/ReadConcernHelper.cs +++ b/src/MongoDB.Driver/Core/Operations/ReadConcernHelper.cs @@ -40,7 +40,7 @@ public static BsonDocument GetReadConcernForSnapshotSession(ICoreSession session Feature.SnapshotReads.ThrowIfNotSupported(connectionDescription.MaxWireVersion); var readConcernDocument = ReadConcern.Snapshot.ToBsonDocument(); - if (session.SnapshotTime != null) // For the first read within a snapshot session, SnapshotTime will be null + if (session.SnapshotTime != null) { readConcernDocument.Add("atClusterTime", session.SnapshotTime); } From fcbefd9abac8c1ed175fc80f6a73a452411ad106 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 18 Sep 2025 10:46:23 +0200 Subject: [PATCH 03/15] Small fix --- src/MongoDB.Driver/IClientSession.cs | 14 +++++++++----- tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs | 4 ++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/MongoDB.Driver/IClientSession.cs b/src/MongoDB.Driver/IClientSession.cs index 26d46cd5967..ed28891abd1 100644 --- a/src/MongoDB.Driver/IClientSession.cs +++ b/src/MongoDB.Driver/IClientSession.cs @@ -21,6 +21,15 @@ namespace MongoDB.Driver { + public static class ClientSessionExtensions + { + //TODO This will need to be moved somewhere else + public static BsonTimestamp GetSnapshotTime(this IClientSessionHandle session) + { + return ((ClientSessionHandle)session).SnapshotTime; + } + } + /// /// The interface for a client session. /// @@ -83,11 +92,6 @@ public interface IClientSession : IDisposable /// IServerSession ServerSession { get; } - /// - /// //TODO - /// - BsonTimestamp SnapshotTime { get; } - /// /// Gets the wrapped core session (intended for internal use only). /// diff --git a/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs b/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs index 8ed9d197968..d5a8eb78b91 100644 --- a/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs +++ b/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs @@ -51,7 +51,7 @@ public void AtClusterTime_should_work() var results = collection.Find(session1, filterDefinition).Sort(sortDefinition).ToList(); AssertOneObj(results); - clusterTime1 = session1.SnapshotTime; + clusterTime1 = session1.GetSnapshotTime(); Assert.NotEqual(null, clusterTime1); } @@ -70,7 +70,7 @@ public void AtClusterTime_should_work() var results = collection.Find(session2, filterDefinition).Sort(sortDefinition).ToList(); AssertOneObj(results); - var clusterTime2 = session2.SnapshotTime; + var clusterTime2 = session2.GetSnapshotTime(); Assert.Equal(clusterTime2, clusterTime1); } From 117276b6dad8af39cff5612f6b54f119a7e8f7b2 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 18 Sep 2025 10:49:30 +0200 Subject: [PATCH 04/15] Added comments --- src/MongoDB.Driver/IClientSession.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/MongoDB.Driver/IClientSession.cs b/src/MongoDB.Driver/IClientSession.cs index ed28891abd1..7e17f1331ac 100644 --- a/src/MongoDB.Driver/IClientSession.cs +++ b/src/MongoDB.Driver/IClientSession.cs @@ -21,9 +21,17 @@ namespace MongoDB.Driver { + /// + /// //TODO + /// public static class ClientSessionExtensions { //TODO This will need to be moved somewhere else + /// + /// //TODO + /// + /// + /// public static BsonTimestamp GetSnapshotTime(this IClientSessionHandle session) { return ((ClientSessionHandle)session).SnapshotTime; From 5d358db5942043d3c74ad1089add1bb98d6d9384 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 18 Sep 2025 10:57:17 +0200 Subject: [PATCH 05/15] Small fix --- tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs b/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs index d5a8eb78b91..7c56a109a66 100644 --- a/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs +++ b/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs @@ -16,6 +16,8 @@ using System.Collections.Generic; using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; +using MongoDB.Driver.Core.Clusters; +using MongoDB.Driver.Core.TestHelpers.XunitExtensions; using Xunit; namespace MongoDB.Driver.Tests; @@ -25,6 +27,7 @@ public class AtClusterTimeTests [Fact] public void AtClusterTime_should_work() { + RequireServer.Check().ClusterType(ClusterType.ReplicaSet); const string collectionName = "atClusterTimeTests"; const string databaseName = "testDb"; From 375a7891ef43cb521180a01bb824fc358fc12711 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 18 Sep 2025 11:30:49 +0200 Subject: [PATCH 06/15] Small fix --- tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs b/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs index 7c56a109a66..eea9d16204d 100644 --- a/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs +++ b/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs @@ -17,6 +17,7 @@ using MongoDB.Bson; using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver.Core.Clusters; +using MongoDB.Driver.Core.Misc; using MongoDB.Driver.Core.TestHelpers.XunitExtensions; using Xunit; @@ -27,7 +28,7 @@ public class AtClusterTimeTests [Fact] public void AtClusterTime_should_work() { - RequireServer.Check().ClusterType(ClusterType.ReplicaSet); + RequireServer.Check().ClusterType(ClusterType.ReplicaSet).Supports(Feature.SnapshotReads); const string collectionName = "atClusterTimeTests"; const string databaseName = "testDb"; From ed34398e454702268e13ef51b7199d6650ffae9c Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 18 Sep 2025 11:43:39 +0200 Subject: [PATCH 07/15] Added missing category --- tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs b/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs index eea9d16204d..a2811242d07 100644 --- a/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs +++ b/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs @@ -23,6 +23,7 @@ namespace MongoDB.Driver.Tests; +[Trait("Category", "Integration")] public class AtClusterTimeTests { [Fact] From 605a2a51f610f3d9714bd9ad9a7c0474c85007cc Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 18 Sep 2025 16:39:54 +0200 Subject: [PATCH 08/15] Added constructor --- .../Core/Bindings/CoreSessionOptions.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs b/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs index 828ba94f1a4..0ee0a1ac0b7 100644 --- a/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs +++ b/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs @@ -52,6 +52,22 @@ public CoreSessionOptions( _snapshotTime = snapshotTime; } + /// + /// Initializes a new instance of the class. + /// + /// if set to true this session is causally consistent] + /// if set to true this session is an implicit session. + /// if set to true this session is a snapshot session. + /// The default transaction options. + public CoreSessionOptions( + bool isCausallyConsistent, + bool isImplicit, + TransactionOptions defaultTransactionOptions, + bool isSnapshot) + : this(isCausallyConsistent, isImplicit, defaultTransactionOptions, isSnapshot, null) + { + } + // public properties /// /// Gets the default transaction options. From e234fbbb4dc07d41217cd93a23f11dc570a211f0 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 18 Sep 2025 18:15:49 +0200 Subject: [PATCH 09/15] Fixed tests --- .../AtClusterTimeTests.cs | 151 ++++++++++++++---- 1 file changed, 121 insertions(+), 30 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs b/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs index a2811242d07..209056c7863 100644 --- a/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs +++ b/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs @@ -18,34 +18,26 @@ using MongoDB.Bson.Serialization.Attributes; using MongoDB.Driver.Core.Clusters; using MongoDB.Driver.Core.Misc; -using MongoDB.Driver.Core.TestHelpers.XunitExtensions; +using MongoDB.Driver.TestHelpers; using Xunit; namespace MongoDB.Driver.Tests; -[Trait("Category", "Integration")] -public class AtClusterTimeTests +public class AtClusterTimeTests : IntegrationTest { - [Fact] - public void AtClusterTime_should_work() + public AtClusterTimeTests(ClassFixture fixture) + : base(fixture, server => server.Supports(Feature.SnapshotReads).ClusterType(ClusterType.ReplicaSet)) { - RequireServer.Check().ClusterType(ClusterType.ReplicaSet).Supports(Feature.SnapshotReads); - const string collectionName = "atClusterTimeTests"; - const string databaseName = "testDb"; - - using var client = DriverTestConfiguration.Client; - var database = client.GetDatabase(databaseName); - database.DropCollection(collectionName); - var collection = database.GetCollection(collectionName); + } - var obj1 = new TestObject { Name = "obj1" }; - collection.InsertOne(obj1); + [Fact] + public void MainTest() + { + var client = Fixture.Client; + var collection = Fixture.Collection; BsonTimestamp clusterTime1; - var filterDefinition = Builders.Filter.Empty; - var sortDefinition = Builders.Sort.Ascending(o => o.Name); - var sessionOptions1 = new ClientSessionOptions { Snapshot = true @@ -53,7 +45,7 @@ public void AtClusterTime_should_work() using (var session1 = client.StartSession(sessionOptions1)) { - var results = collection.Find(session1, filterDefinition).Sort(sortDefinition).ToList(); + var results = GetTestObjects(collection, session1); AssertOneObj(results); clusterTime1 = session1.GetSnapshotTime(); @@ -72,7 +64,7 @@ public void AtClusterTime_should_work() //Snapshot read session at clusterTime1 should not see obj2 using (var session2 = client.StartSession(sessionOptions2)) { - var results = collection.Find(session2, filterDefinition).Sort(sortDefinition).ToList(); + var results = GetTestObjects(collection, session2); AssertOneObj(results); var clusterTime2 = session2.GetSnapshotTime(); @@ -87,28 +79,127 @@ public void AtClusterTime_should_work() //Snapshot read session without cluster time should see obj2 using (var session3 = client.StartSession(sessionOptions3)) { - var results = collection.Find(session3, filterDefinition).Sort(sortDefinition).ToList(); + var results = GetTestObjects(collection, session3); AssertTwoObjs(results); - var clusterTime3 = session3.WrappedCoreSession.SnapshotTime; + var clusterTime3 = session3.GetSnapshotTime(); Assert.NotEqual(clusterTime3, clusterTime1); } + } - void AssertOneObj(List objs) + [Fact] + public void IncreasedTimestamp() + { + var client = Fixture.Client; + var collection = Fixture.Collection; + + BsonTimestamp clusterTime1; + + var sessionOptions1 = new ClientSessionOptions + { + Snapshot = true + }; + + using (var session1 = client.StartSession(sessionOptions1)) { - Assert.Equal(1, objs.Count); - Assert.Equal("obj1", objs[0].Name); + var results = GetTestObjects(collection, session1); + AssertOneObj(results); + + clusterTime1 = session1.GetSnapshotTime(); + Assert.NotEqual(null, clusterTime1); } - void AssertTwoObjs(List objs) + var obj2 = new TestObject { Name = "obj2" }; + collection.InsertOne(obj2); + + var modifiedClusterTime = new BsonTimestamp(clusterTime1.Value + 1); + var sessionOptions2 = new ClientSessionOptions { - Assert.Equal(2, objs.Count); - Assert.Equal("obj1", objs[0].Name); - Assert.Equal("obj2", objs[1].Name); + Snapshot = true, + SnapshotTime = modifiedClusterTime + }; + + //Snapshot read session at clusterTime1+1 should see obj2 + using (var session2 = client.StartSession(sessionOptions2)) + { + var results = GetTestObjects(collection, session2); + AssertTwoObjs(results); + + var clusterTime2 = session2.GetSnapshotTime(); + Assert.Equal(modifiedClusterTime, clusterTime2); } } - private class TestObject + [Fact] + public void DecreasedTimestamp() + { + var client = Fixture.Client; + var collection = Fixture.Collection; + + BsonTimestamp clusterTime1; + + var sessionOptions1 = new ClientSessionOptions + { + Snapshot = true + }; + + using (var session1 = client.StartSession(sessionOptions1)) + { + var results = GetTestObjects(collection, session1); + AssertOneObj(results); + + clusterTime1 = session1.GetSnapshotTime(); + Assert.NotEqual(null, clusterTime1); + } + + var obj2 = new TestObject { Name = "obj2" }; + collection.InsertOne(obj2); + + var modifiedClusterTime = new BsonTimestamp(clusterTime1.Value - 1); + var sessionOptions2 = new ClientSessionOptions + { + Snapshot = true, + SnapshotTime = modifiedClusterTime + }; + + //Snapshot read session at clusterTime1-1 should not see obj2 + using (var session2 = client.StartSession(sessionOptions2)) + { + var results = GetTestObjects(collection, session2); + Assert.Equal(0, results.Count); + + var clusterTime2 = session2.GetSnapshotTime(); + Assert.Equal(modifiedClusterTime, clusterTime2); + } + } + + List GetTestObjects(IMongoCollection collection, IClientSessionHandle session) + { + var filterDefinition = Builders.Filter.Empty; + var sortDefinition = Builders.Sort.Ascending(o => o.Name); + return collection.Find(session, filterDefinition).Sort(sortDefinition).ToList(); + } + + void AssertOneObj(List objs) + { + Assert.Equal(1, objs.Count); + Assert.Equal("obj1", objs[0].Name); + } + + void AssertTwoObjs(List objs) + { + Assert.Equal(2, objs.Count); + Assert.Equal("obj1", objs[0].Name); + Assert.Equal("obj2", objs[1].Name); + } + + public class ClassFixture : MongoCollectionFixture + { + public override bool InitializeDataBeforeEachTestCase => true; + protected override IEnumerable InitialData => [new() { Name = "obj1" }] ; + } + + public class TestObject { [BsonId] public ObjectId Id { get; set; } From 7d320319ef74764f2b19ac4d49f4c70098d48509 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Wed, 24 Sep 2025 17:52:39 +0200 Subject: [PATCH 10/15] Added comment. --- src/MongoDB.Driver/ClientSessionOptions.cs | 3 ++- .../Core/Bindings/CoreSessionOptions.cs | 2 +- .../UnifiedGetSnapshotOperation.cs | 21 +++++++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedGetSnapshotOperation.cs diff --git a/src/MongoDB.Driver/ClientSessionOptions.cs b/src/MongoDB.Driver/ClientSessionOptions.cs index e670b274116..3ff667ada60 100644 --- a/src/MongoDB.Driver/ClientSessionOptions.cs +++ b/src/MongoDB.Driver/ClientSessionOptions.cs @@ -48,7 +48,8 @@ public class ClientSessionOptions public bool Snapshot { get; set;} /// - /// //TODO + /// Gets or sets the snapshot time. If set, Snapshot must be true. + /// The snapshot time /// public BsonTimestamp SnapshotTime { get; set; } diff --git a/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs b/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs index 0ee0a1ac0b7..d0bc03f2fe6 100644 --- a/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs +++ b/src/MongoDB.Driver/Core/Bindings/CoreSessionOptions.cs @@ -37,7 +37,7 @@ public class CoreSessionOptions /// if set to true this session is an implicit session. /// if set to true this session is a snapshot session. /// The default transaction options. - /// //TODO + /// The snapshot time. If this is set, isSnapshot must be true. public CoreSessionOptions( bool isCausallyConsistent = false, bool isImplicit = false, diff --git a/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedGetSnapshotOperation.cs b/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedGetSnapshotOperation.cs new file mode 100644 index 00000000000..8f2c9748068 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedGetSnapshotOperation.cs @@ -0,0 +1,21 @@ +/* Copyright 2010-present MongoDB Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +namespace MongoDB.Driver.Tests.UnifiedTestOperations; + +public class UnifiedGetSnapshotOperation +{ + +} \ No newline at end of file From ee85c62c7be228b6c3b55c97983faca1ebe5781a Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 25 Sep 2025 09:54:03 +0200 Subject: [PATCH 11/15] Added test infrastructure --- .../sessions/tests/snapshot-sessions.json | 141 ++++++++++++++++++ .../sessions/tests/snapshot-sessions.yml | 68 +++++++++ .../UnifiedTestOperations/UnifiedEntityMap.cs | 3 + .../UnifiedGetSnapshotOperation.cs | 52 ++++++- .../UnifiedTestOperationFactory.cs | 1 + 5 files changed, 263 insertions(+), 2 deletions(-) diff --git a/specifications/sessions/tests/snapshot-sessions.json b/specifications/sessions/tests/snapshot-sessions.json index 260f8b6f489..89460ab6559 100644 --- a/specifications/sessions/tests/snapshot-sessions.json +++ b/specifications/sessions/tests/snapshot-sessions.json @@ -988,6 +988,147 @@ } } ] + }, + { + "description": "Find operation with snapshot and snapshot time", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": {} + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 11 + } + ] + }, + { + "name": "getSnapshotTime", + "object": "session0", + "saveResultAsEntity": "savedSnapshotTime" + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "session": { + "id": "session2", + "client": "client0", + "sessionOptions": { + "snapshot": true, + "snapshotTime": "savedSnapshotTime" + } + } + } + ] + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session2", + "filter": {} + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 11 + } + ] + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 11 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "$$exists": false + } + }, + "databaseName": "database0" + } + } + ] + } + ] } ] } diff --git a/specifications/sessions/tests/snapshot-sessions.yml b/specifications/sessions/tests/snapshot-sessions.yml index bcf0f7eec6b..f842e44bdb6 100644 --- a/specifications/sessions/tests/snapshot-sessions.yml +++ b/specifications/sessions/tests/snapshot-sessions.yml @@ -480,3 +480,71 @@ tests: isError: true isClientError: true errorContains: Transactions are not supported in snapshot sessions + +- description: Find operation with snapshot and snapshot time + operations: + - name: find + object: collection0 + arguments: + session: session0 + filter: {} + expectResult: + - { _id: 1, x: 11 } + - { _id: 2, x: 11 } + - name: getSnapshotTime + object: session0 + saveResultAsEntity: &savedSnapshotTime savedSnapshotTime + - name: insertOne + object: collection0 + arguments: + document: { _id: 3, x: 33 } + - name: createEntities + object: testRunner + arguments: + entities: + - session: + id: session2 + client: client0 + sessionOptions: + snapshot: true + snapshotTime: *savedSnapshotTime + - name: find + object: collection0 + arguments: + session: session2 + filter: {} + expectResult: + - { _id: 1, x: 11 } + - { _id: 2, x: 11 } + - name: find + object: collection0 + arguments: + filter: {} + expectResult: + - { _id: 1, x: 11 } + - { _id: 2, x: 11 } + - { _id: 3, x: 33 } + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + find: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + databaseName: database0 + - commandStartedEvent: + command: + find: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: savedSnapshotTime } + databaseName: database0 + - commandStartedEvent: + command: + find: collection0 + readConcern: + "$$exists": false + databaseName: database0 diff --git a/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedEntityMap.cs b/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedEntityMap.cs index e5ea553ff15..cea7cc56391 100644 --- a/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedEntityMap.cs +++ b/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedEntityMap.cs @@ -1056,6 +1056,9 @@ private IClientSessionHandle CreateSession(BsonDocument entity, Dictionary GetSnapshotTime(); + + //TODO Do we necessarily need an async version of this...? + public Task ExecuteAsync(CancellationToken cancellationToken) => Task.FromResult(GetSnapshotTime()); + + private OperationResult GetSnapshotTime() + { + try + { + return OperationResult.FromResult(_session.GetSnapshotTime()); + } + catch (Exception exception) + { + return OperationResult.FromException(exception); + } + } +} + +public class UnifiedGetSnapshotOperationBuilder { - + private readonly UnifiedEntityMap _entityMap; + + public UnifiedGetSnapshotOperationBuilder(UnifiedEntityMap entityMap) + { + _entityMap = entityMap; + } + + public UnifiedGetSnapshotOperation Build(string targetSessionId, BsonDocument arguments) + { + if (arguments != null) + { + throw new FormatException("GetSnapshotTime is not expected to contain arguments."); + } + + var session = _entityMap.Sessions[targetSessionId]; + return new UnifiedGetSnapshotOperation(session); + } } \ No newline at end of file diff --git a/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedTestOperationFactory.cs b/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedTestOperationFactory.cs index 32e6ec08f3d..099ae07359f 100644 --- a/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedTestOperationFactory.cs +++ b/tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedTestOperationFactory.cs @@ -133,6 +133,7 @@ public IUnifiedTestOperation CreateOperation(string operationName, string target "abortTransaction" => new UnifiedAbortTransactionOperationBuilder(_entityMap).Build(targetEntityId, operationArguments), "commitTransaction" => new UnifiedCommitTransactionOperationBuilder(_entityMap).Build(targetEntityId, operationArguments), "endSession" => new UnifiedEndSessionOperationBuilder(_entityMap).Build(targetEntityId, operationArguments), + "getSnapshotTime" => new UnifiedGetSnapshotOperationBuilder(_entityMap).Build(targetEntityId, operationArguments), "startTransaction" => new UnifiedStartTransactionOperationBuilder(_entityMap).Build(targetEntityId, operationArguments), "withTransaction" => new UnifiedWithTransactionOperationBuilder(_entityMap).Build(targetEntityId, operationArguments), _ => throw new FormatException($"Invalid method name: '{operationName}'."), From f647078859fef7d6f3dce863fad03a0cc7380a11 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 25 Sep 2025 10:32:40 +0200 Subject: [PATCH 12/15] Added prose test --- .../sessions/tests/snapshot-sessions.json | 32 +++++++++++++++++++ .../sessions/tests/snapshot-sessions.yml | 21 ++++++++++-- src/MongoDB.Driver/MongoClient.cs | 14 +++++--- .../sessions/SessionsProseTests.cs | 18 +++++++++++ 4 files changed, 79 insertions(+), 6 deletions(-) diff --git a/specifications/sessions/tests/snapshot-sessions.json b/specifications/sessions/tests/snapshot-sessions.json index 89460ab6559..48aecc16ecc 100644 --- a/specifications/sessions/tests/snapshot-sessions.json +++ b/specifications/sessions/tests/snapshot-sessions.json @@ -1061,6 +1061,24 @@ } ] }, + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session2", + "filter": {} + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 11 + } + ] + }, { "name": "find", "object": "collection0", @@ -1115,6 +1133,20 @@ "databaseName": "database0" } }, + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + }, + "databaseName": "database0" + } + }, { "commandStartedEvent": { "command": { diff --git a/specifications/sessions/tests/snapshot-sessions.yml b/specifications/sessions/tests/snapshot-sessions.yml index f842e44bdb6..74c6f928313 100644 --- a/specifications/sessions/tests/snapshot-sessions.yml +++ b/specifications/sessions/tests/snapshot-sessions.yml @@ -378,7 +378,7 @@ tests: fieldName: x filter: {} session: session0 - expectResult: [ 11 ] + expectResult: [ 11 ] expectEvents: - client: client0 events: @@ -508,6 +508,16 @@ tests: sessionOptions: snapshot: true snapshotTime: *savedSnapshotTime + - name: find + object: collection0 + arguments: + session: session2 + filter: {} + expectResult: + - { _id: 1, x: 11 } + - { _id: 2, x: 11 } + ## Calling find again to verify that atClusterTime/snapshotTime has not been modified after the first query + ## as it would happen if snapshotTime had not been specified - name: find object: collection0 arguments: @@ -540,7 +550,14 @@ tests: find: collection0 readConcern: level: snapshot - atClusterTime: { $$matchesEntity: savedSnapshotTime } + atClusterTime: { $$matchesEntity: *savedSnapshotTime } + databaseName: database0 + - commandStartedEvent: + command: + find: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: *savedSnapshotTime } databaseName: database0 - commandStartedEvent: command: diff --git a/src/MongoDB.Driver/MongoClient.cs b/src/MongoDB.Driver/MongoClient.cs index bac494e1b9a..833991d511c 100644 --- a/src/MongoDB.Driver/MongoClient.cs +++ b/src/MongoDB.Driver/MongoClient.cs @@ -622,12 +622,18 @@ private RenderArgs GetRenderArgs() private IClientSessionHandle StartSession(ClientSessionOptions options) { - if (options != null && options.Snapshot && options.CausalConsistency == true) + if (options != null) { - throw new NotSupportedException("Combining both causal consistency and snapshot options is not supported."); - } + if (options.SnapshotTime != null && !options.Snapshot) + { + throw new NotSupportedException("Specifying a snapshot time requires snapshot to be true."); + } - //TODO Throw an exception if SnapshotTime is set and Snapshot is not true. + if (options.Snapshot && options.CausalConsistency == true) + { + throw new NotSupportedException("Combining both causal consistency and snapshot options is not supported."); + } + } options ??= new ClientSessionOptions(); if (_settings.Timeout.HasValue && options.DefaultTransactionOptions?.Timeout == null) diff --git a/tests/MongoDB.Driver.Tests/Specifications/sessions/SessionsProseTests.cs b/tests/MongoDB.Driver.Tests/Specifications/sessions/SessionsProseTests.cs index 649e2ae2df9..40244a88523 100644 --- a/tests/MongoDB.Driver.Tests/Specifications/sessions/SessionsProseTests.cs +++ b/tests/MongoDB.Driver.Tests/Specifications/sessions/SessionsProseTests.cs @@ -357,6 +357,24 @@ public void Ensure_cluster_times_are_not_gossiped_on_SDAM_commands() commandStartedEvents[0].Command["$clusterTime"].Should().Be(clusterTime); } + [Fact] + public void If_SnapshotTime_is_set_Snapshot_must_be_true() + { + RequireServer.Check(); + + var sessionOptions = new ClientSessionOptions + { + Snapshot = false, + SnapshotTime = new BsonTimestamp(1, 1) + }; + + var mongoClient = DriverTestConfiguration.Client; + + var exception = Record.Exception(() => mongoClient.StartSession(sessionOptions)); + exception.Should().BeOfType(); + } + + private sealed class MongocryptdContext : IDisposable { public IMongoClient MongoClient { get; } From 01faa2798af4aa43eb346944bfc8bab932f9c1bf Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 25 Sep 2025 10:40:51 +0200 Subject: [PATCH 13/15] Added distinct test --- .../sessions/tests/snapshot-sessions.json | 146 ++++++++++++++++++ .../sessions/tests/snapshot-sessions.yml | 80 ++++++++++ .../AtClusterTimeTests.cs | 2 +- 3 files changed, 227 insertions(+), 1 deletion(-) diff --git a/specifications/sessions/tests/snapshot-sessions.json b/specifications/sessions/tests/snapshot-sessions.json index 48aecc16ecc..b35a535d0fc 100644 --- a/specifications/sessions/tests/snapshot-sessions.json +++ b/specifications/sessions/tests/snapshot-sessions.json @@ -1161,6 +1161,152 @@ ] } ] + }, + { + "description": "Distinct operation with snapshot and snapshot time", + "operations": [ + { + "name": "distinct", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": {}, + "fieldName": "x" + }, + "expectResult": [ + 11 + ] + }, + { + "name": "getSnapshotTime", + "object": "session0", + "saveResultAsEntity": "savedSnapshotTime" + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "session": { + "id": "session2", + "client": "client0", + "sessionOptions": { + "snapshot": true, + "snapshotTime": "savedSnapshotTime" + } + } + } + ] + } + }, + { + "name": "distinct", + "object": "collection0", + "arguments": { + "session": "session2", + "filter": {}, + "fieldName": "x" + }, + "expectResult": [ + 11 + ] + }, + { + "name": "distinct", + "object": "collection0", + "arguments": { + "session": "session2", + "filter": {}, + "fieldName": "x" + }, + "expectResult": [ + 11 + ] + }, + { + "name": "distinct", + "object": "collection0", + "arguments": { + "filter": {}, + "fieldName": "x" + }, + "expectResult": [ + 11, + 33 + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "$$exists": false + } + }, + "databaseName": "database0" + } + } + ] + } + ] } ] } diff --git a/specifications/sessions/tests/snapshot-sessions.yml b/specifications/sessions/tests/snapshot-sessions.yml index 74c6f928313..22689ca3085 100644 --- a/specifications/sessions/tests/snapshot-sessions.yml +++ b/specifications/sessions/tests/snapshot-sessions.yml @@ -565,3 +565,83 @@ tests: readConcern: "$$exists": false databaseName: database0 + +- description: Distinct operation with snapshot and snapshot time + operations: + - name: distinct + object: collection0 + arguments: + session: session0 + filter: {} + fieldName: x + expectResult: [11] + - name: getSnapshotTime + object: session0 + saveResultAsEntity: &savedSnapshotTime savedSnapshotTime + - name: insertOne + object: collection0 + arguments: + document: { _id: 3, x: 33 } + - name: createEntities + object: testRunner + arguments: + entities: + - session: + id: session2 + client: client0 + sessionOptions: + snapshot: true + snapshotTime: *savedSnapshotTime + - name: distinct + object: collection0 + arguments: + session: session2 + filter: {} + fieldName: x + expectResult: [11] + ## Calling find again to verify that atClusterTime/snapshotTime has not been modified after the first query + ## as it would happen if snapshotTime had not been specified + - name: distinct + object: collection0 + arguments: + session: session2 + filter: {} + fieldName: x + expectResult: [11] + - name: distinct + object: collection0 + arguments: + filter: {} + fieldName: x + expectResult: [11, 33] + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + databaseName: database0 + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: *savedSnapshotTime } + databaseName: database0 + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: *savedSnapshotTime } + databaseName: database0 + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + "$$exists": false + databaseName: database0 \ No newline at end of file diff --git a/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs b/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs index 209056c7863..a97e96343bf 100644 --- a/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs +++ b/tests/MongoDB.Driver.Tests/AtClusterTimeTests.cs @@ -22,7 +22,7 @@ using Xunit; namespace MongoDB.Driver.Tests; - +//TODO This file will need to be deleted, but it's useful for testing at the moment public class AtClusterTimeTests : IntegrationTest { public AtClusterTimeTests(ClassFixture fixture) From a4d782bc9348a4db36d95052b607cdd2803e2dfe Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:06:17 +0200 Subject: [PATCH 14/15] Added tests --- .../sessions/tests/snapshot-sessions.json | 485 ++++++++++++++++++ .../sessions/tests/snapshot-sessions.yml | 247 ++++++++- 2 files changed, 731 insertions(+), 1 deletion(-) diff --git a/specifications/sessions/tests/snapshot-sessions.json b/specifications/sessions/tests/snapshot-sessions.json index b35a535d0fc..a831852e71e 100644 --- a/specifications/sessions/tests/snapshot-sessions.json +++ b/specifications/sessions/tests/snapshot-sessions.json @@ -1307,6 +1307,491 @@ ] } ] + }, + { + "description": "Aggregate operation with snapshot and snapshot time", + "operations": [ + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "session": "session0", + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "getSnapshotTime", + "object": "session0", + "saveResultAsEntity": "savedSnapshotTime" + }, + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "After" + }, + "expectResult": { + "_id": 1, + "x": 12 + } + }, + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "session": { + "id": "session2", + "client": "client0", + "sessionOptions": { + "snapshot": true, + "snapshotTime": "savedSnapshotTime" + } + } + } + ] + } + }, + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "session": "session2", + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "session": "session2", + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ] + }, + "expectResult": [ + { + "_id": 1, + "x": 12 + } + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "$$exists": false + } + }, + "databaseName": "database0" + } + } + ] + } + ] + }, + { + "description": "countDocuments operation with snapshot and snapshot time", + "operations": [ + { + "name": "countDocuments", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": {} + }, + "expectResult": 2 + }, + { + "name": "getSnapshotTime", + "object": "session0", + "saveResultAsEntity": "savedSnapshotTime" + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 3, + "x": 33 + } + } + }, + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "session": { + "id": "session2", + "client": "client0", + "sessionOptions": { + "snapshot": true, + "snapshotTime": "savedSnapshotTime" + } + } + } + ] + } + }, + { + "name": "countDocuments", + "object": "collection0", + "arguments": { + "session": "session2", + "filter": {} + }, + "expectResult": 2 + }, + { + "name": "countDocuments", + "object": "collection0", + "arguments": { + "session": "session2", + "filter": {} + }, + "expectResult": 2 + }, + { + "name": "countDocuments", + "object": "collection0", + "arguments": { + "filter": {} + }, + "expectResult": 3 + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$matchesEntity": "savedSnapshotTime" + } + } + }, + "databaseName": "database0" + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "$$exists": false + } + }, + "databaseName": "database0" + } + } + ] + } + ] + }, + { + "description": "Mixed operation with snapshot and snapshotTime", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "getSnapshotTime", + "object": "session0", + "saveResultAsEntity": "savedSnapshotTime" + }, + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "After" + }, + "expectResult": { + "_id": 1, + "x": 12 + } + }, + { + "name": "createEntities", + "object": "testRunner", + "arguments": { + "entities": [ + { + "session": { + "id": "session2", + "client": "client0", + "sessionOptions": { + "snapshot": true, + "snapshotTime": "savedSnapshotTime" + } + } + } + ] + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "_id": 1 + } + }, + "expectResult": [ + { + "_id": 1, + "x": 12 + } + ] + }, + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "pipeline": [ + { + "$match": { + "_id": 1 + } + } + ], + "session": "session0" + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] + }, + { + "name": "distinct", + "object": "collection0", + "arguments": { + "fieldName": "x", + "filter": {}, + "session": "session0" + }, + "expectResult": [ + 11 + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": false + } + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collection0", + "readConcern": { + "$$exists": false + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "aggregate": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": true + } + } + } + } + }, + { + "commandStartedEvent": { + "command": { + "distinct": "collection0", + "readConcern": { + "level": "snapshot", + "atClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] } ] } diff --git a/specifications/sessions/tests/snapshot-sessions.yml b/specifications/sessions/tests/snapshot-sessions.yml index 22689ca3085..15ec3fa047c 100644 --- a/specifications/sessions/tests/snapshot-sessions.yml +++ b/specifications/sessions/tests/snapshot-sessions.yml @@ -309,6 +309,7 @@ tests: atClusterTime: "$$exists": true +## This test seems to be wrong - description: countDocuments operation with snapshot operations: - name: countDocuments @@ -644,4 +645,248 @@ tests: distinct: collection0 readConcern: "$$exists": false - databaseName: database0 \ No newline at end of file + databaseName: database0 + +- description: Aggregate operation with snapshot and snapshot time + operations: + - name: aggregate + object: collection0 + arguments: + session: session0 + pipeline: + - "$match": { _id: 1 } + expectResult: + - { _id: 1, x: 11 } + - name: getSnapshotTime + object: session0 + saveResultAsEntity: &savedSnapshotTime savedSnapshotTime + - name: findOneAndUpdate + object: collection0 + arguments: + filter: { _id: 1 } + update: { $inc: { x: 1 } } + returnDocument: After + expectResult: { _id: 1, x: 12 } + - name: createEntities + object: testRunner + arguments: + entities: + - session: + id: session2 + client: client0 + sessionOptions: + snapshot: true + snapshotTime: *savedSnapshotTime + - name: aggregate + object: collection0 + arguments: + session: session2 + pipeline: + - "$match": { _id: 1 } + expectResult: + - { _id: 1, x: 11 } + ## Calling find again to verify that atClusterTime/snapshotTime has not been modified after the first query + ## as it would happen if snapshotTime had not been specified + - name: aggregate + object: collection0 + arguments: + session: session2 + pipeline: + - "$match": { _id: 1 } + expectResult: + - { _id: 1, x: 11 } + - name: aggregate + object: collection0 + arguments: + pipeline: + - "$match": { _id: 1 } + expectResult: + - { _id: 1, x: 12 } + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + databaseName: database0 + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: *savedSnapshotTime } + databaseName: database0 + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: *savedSnapshotTime } + databaseName: database0 + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + "$$exists": false + databaseName: database0 + +- description: countDocuments operation with snapshot and snapshot time + operations: + - name: countDocuments + object: collection0 + arguments: + session: session0 + filter: {} + expectResult: 2 + - name: getSnapshotTime + object: session0 + saveResultAsEntity: &savedSnapshotTime savedSnapshotTime + - name: insertOne + object: collection0 + arguments: + document: { _id: 3, x: 33 } + - name: createEntities + object: testRunner + arguments: + entities: + - session: + id: session2 + client: client0 + sessionOptions: + snapshot: true + snapshotTime: *savedSnapshotTime + - name: countDocuments + object: collection0 + arguments: + session: session2 + filter: {} + expectResult: 2 + ## Calling find again to verify that atClusterTime/snapshotTime has not been modified after the first query + ## as it would happen if snapshotTime had not been specified + - name: countDocuments + object: collection0 + arguments: + session: session2 + filter: {} + expectResult: 2 + - name: countDocuments + object: collection0 + arguments: + filter: {} + expectResult: 3 + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + databaseName: database0 + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: *savedSnapshotTime } + databaseName: database0 + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: { $$matchesEntity: *savedSnapshotTime } + databaseName: database0 + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + "$$exists": false + databaseName: database0 + +- description: Mixed operation with snapshot and snapshotTime + operations: + - name: find + object: collection0 + arguments: + session: session0 + filter: { _id: 1 } + expectResult: + - { _id: 1, x: 11 } + - name: getSnapshotTime + object: session0 + saveResultAsEntity: &savedSnapshotTime savedSnapshotTime + - name: findOneAndUpdate + object: collection0 + arguments: + filter: { _id: 1 } + update: { $inc: { x: 1 } } + returnDocument: After + expectResult: { _id: 1, x: 12 } + - name: createEntities + object: testRunner + arguments: + entities: + - session: + id: session2 + client: client0 + sessionOptions: + snapshot: true + snapshotTime: *savedSnapshotTime + - name: find + object: collection0 + arguments: + filter: { _id: 1 } + expectResult: + - { _id: 1, x: 12 } + - name: aggregate + object: collection0 + arguments: + pipeline: + - "$match": + _id: 1 + session: session0 + expectResult: + - { _id: 1, x: 11 } + - name: distinct + object: collection0 + arguments: + fieldName: x + filter: {} + session: session0 + expectResult: [ 11 ] + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + find: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": false + - commandStartedEvent: + command: + find: collection0 + readConcern: + "$$exists": false + - commandStartedEvent: + command: + aggregate: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": true + - commandStartedEvent: + command: + distinct: collection0 + readConcern: + level: snapshot + atClusterTime: + "$$exists": true \ No newline at end of file From 9270a5ffd706bd88ef171b2dfeb4eb982878ea66 Mon Sep 17 00:00:00 2001 From: Ferdinando Papale <4850119+papafe@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:10:13 +0200 Subject: [PATCH 15/15] Corrected tests --- specifications/sessions/tests/snapshot-sessions.json | 8 ++++---- specifications/sessions/tests/snapshot-sessions.yml | 10 ++++------ 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/specifications/sessions/tests/snapshot-sessions.json b/specifications/sessions/tests/snapshot-sessions.json index a831852e71e..8f806ea7595 100644 --- a/specifications/sessions/tests/snapshot-sessions.json +++ b/specifications/sessions/tests/snapshot-sessions.json @@ -1714,7 +1714,7 @@ } } ], - "session": "session0" + "session": "session2" }, "expectResult": [ { @@ -1729,7 +1729,7 @@ "arguments": { "fieldName": "x", "filter": {}, - "session": "session0" + "session": "session2" }, "expectResult": [ 11 @@ -1770,7 +1770,7 @@ "readConcern": { "level": "snapshot", "atClusterTime": { - "$$exists": true + "$$matchesEntity": "savedSnapshotTime" } } } @@ -1783,7 +1783,7 @@ "readConcern": { "level": "snapshot", "atClusterTime": { - "$$exists": true + "$$matchesEntity": "savedSnapshotTime" } } } diff --git a/specifications/sessions/tests/snapshot-sessions.yml b/specifications/sessions/tests/snapshot-sessions.yml index 15ec3fa047c..48cf415b4aa 100644 --- a/specifications/sessions/tests/snapshot-sessions.yml +++ b/specifications/sessions/tests/snapshot-sessions.yml @@ -851,7 +851,7 @@ tests: pipeline: - "$match": _id: 1 - session: session0 + session: session2 expectResult: - { _id: 1, x: 11 } - name: distinct @@ -859,7 +859,7 @@ tests: arguments: fieldName: x filter: {} - session: session0 + session: session2 expectResult: [ 11 ] expectEvents: - client: client0 @@ -881,12 +881,10 @@ tests: aggregate: collection0 readConcern: level: snapshot - atClusterTime: - "$$exists": true + atClusterTime: { $$matchesEntity: *savedSnapshotTime } - commandStartedEvent: command: distinct: collection0 readConcern: level: snapshot - atClusterTime: - "$$exists": true \ No newline at end of file + atClusterTime: { $$matchesEntity: *savedSnapshotTime } \ No newline at end of file