From ec0b2df2052b386b397aa4ecda529bf8bc84ab84 Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Tue, 28 Apr 2026 21:08:07 -0700 Subject: [PATCH 01/11] DRIVERS-3274 Send afterClusterTime on writes in causally-consistent sessions. --- .../causal-consistency/causal-consistency.md | 6 +- .../causal-consistency-write-commands.json | 923 ++++++++++++++++++ .../causal-consistency-write-commands.yml | 320 ++++++ source/unified-test-format/tests/Makefile | 5 + 4 files changed, 1251 insertions(+), 3 deletions(-) create mode 100644 source/causal-consistency/tests/causal-consistency-write-commands.json create mode 100644 source/causal-consistency/tests/causal-consistency-write-commands.yml diff --git a/source/causal-consistency/causal-consistency.md b/source/causal-consistency/causal-consistency.md index 026619b7fb..33c80bc330 100644 --- a/source/causal-consistency/causal-consistency.md +++ b/source/causal-consistency/causal-consistency.md @@ -200,7 +200,7 @@ There are no new server commands related to causal consistency. Instead, causal The server reports the `operationTime` whether the operation succeeded or not and drivers MUST save the `operationTime` in the `ClientSession` whether the operation succeeded or not. 2. Passing that `operationTime` in the `afterClusterTime` field of the `readConcern` field for subsequent causally - consistent read operations (for all commands that support a `readConcern`) + consistent read and write operations (for all commands that support a `readConcern`) 3. Gossiping clusterTime (described in the Driver Session Specification) ## Server Command Responses @@ -230,10 +230,10 @@ standalone node are causally consistent automatically because there is only one When connected to a deployment that supports cluster times the command response also includes a field called `$clusterTime` that drivers MUST use to gossip the cluster time. See the Sessions Specification for details. -## Causally consistent read commands +## Causally consistent read and write commands For causal consistency the driver MUST send the `operationTime` saved in the `ClientSession` as the value of the -`afterClusterTime` field of the `readConcern` field: +`afterClusterTime` field of the `readConcern` field for read and write commands: ```typescript { diff --git a/source/causal-consistency/tests/causal-consistency-write-commands.json b/source/causal-consistency/tests/causal-consistency-write-commands.json new file mode 100644 index 0000000000..fa320ded2c --- /dev/null +++ b/source/causal-consistency/tests/causal-consistency-write-commands.json @@ -0,0 +1,923 @@ +{ + "description": "causal consistency write commands include afterClusterTime", + "schemaVersion": "1.3", + "runOnRequirements": [ + { + "minServerVersion": "3.6", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "uriOptions": { + "retryWrites": false + }, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "causal-consistency-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + }, + { + "session": { + "id": "session0", + "client": "client0" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "causal-consistency-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "insertOne includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "findOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "session": "session0", + "document": { + "_id": 4 + } + }, + "expectResult": { + "$$unsetOrMatches": { + "insertedId": { + "$$unsetOrMatches": 4 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "insert", + "command": { + "insert": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "insertMany includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "findOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "insertMany", + "object": "collection0", + "arguments": { + "session": "session0", + "documents": [ + { + "_id": 4 + }, + { + "_id": 5 + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "insert", + "command": { + "insert": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "updateOne includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "findOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$set": { + "x": 100 + } + } + }, + "expectResult": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "update", + "command": { + "update": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "updateMany includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "findOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "updateMany", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$gt": 0 + } + }, + "update": { + "$set": { + "updated": true + } + } + }, + "expectResult": { + "matchedCount": 3, + "modifiedCount": 3, + "upsertedCount": 0 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "update", + "command": { + "update": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "replaceOne includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "findOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "replaceOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "replacement": { + "x": 100 + } + }, + "expectResult": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedCount": 0 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "update", + "command": { + "update": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "deleteOne includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "findOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "deleteOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "deletedCount": 1 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "delete", + "command": { + "delete": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "deleteMany includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "findOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "deleteMany", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$gt": 0 + } + } + }, + "expectResult": { + "deletedCount": 3 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "delete", + "command": { + "delete": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "findOneAndUpdate includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "findOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$set": { + "x": 100 + } + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "findAndModify", + "command": { + "findAndModify": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "findOneAndDelete includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "findOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "findOneAndDelete", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "findAndModify", + "command": { + "findAndModify": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "findOneAndReplace includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "findOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "findOneAndReplace", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "replacement": { + "x": 100 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "findAndModify", + "command": { + "findAndModify": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "bulkWrite includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "findOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "bulkWrite", + "object": "collection0", + "arguments": { + "session": "session0", + "requests": [ + { + "insertOne": { + "document": { + "_id": 4 + } + } + }, + { + "updateOne": { + "filter": { + "_id": 2 + }, + "update": { + "$set": { + "x": 100 + } + } + } + }, + { + "deleteOne": { + "filter": { + "_id": 3 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "insert", + "command": { + "insert": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "update", + "command": { + "update": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "delete", + "command": { + "delete": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + } + ] +} diff --git a/source/causal-consistency/tests/causal-consistency-write-commands.yml b/source/causal-consistency/tests/causal-consistency-write-commands.yml new file mode 100644 index 0000000000..5e93894e00 --- /dev/null +++ b/source/causal-consistency/tests/causal-consistency-write-commands.yml @@ -0,0 +1,320 @@ +description: "causal consistency write commands include afterClusterTime" + +schemaVersion: "1.3" + +runOnRequirements: + - minServerVersion: "3.6" + topologies: [replicaset, sharded, load-balanced] + +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + uriOptions: + retryWrites: false + observeEvents: [commandStartedEvent] + - database: + id: &database0 database0 + client: *client0 + databaseName: &databaseName causal-consistency-tests + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collectionName test + - session: + id: &session0 session0 + client: *client0 + +initialData: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + +# In a causally consistent session, once an operationTime has been established by a prior +# operation, subsequent write commands MUST include readConcern.afterClusterTime so the +# server can apply the write causally after the previously-observed data. + +tests: + - description: "insertOne includes afterClusterTime in causally consistent session" + operations: + - &findOne + name: findOne + object: *collection0 + arguments: + session: *session0 + filter: { _id: 1 } + expectResult: { _id: 1, x: 11 } + - name: insertOne + object: *collection0 + arguments: + session: *session0 + document: { _id: 4 } + expectResult: + $$unsetOrMatches: { insertedId: { $$unsetOrMatches: 4 } } + expectEvents: + - client: *client0 + events: + - &findEvent + commandStartedEvent: + commandName: find + command: + find: *collectionName + readConcern: { $$exists: false } + lsid: { $$sessionLsid: *session0 } + - commandStartedEvent: + commandName: insert + command: + insert: *collectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } + + - description: "insertMany includes afterClusterTime in causally consistent session" + operations: + - *findOne + - name: insertMany + object: *collection0 + arguments: + session: *session0 + documents: + - { _id: 4 } + - { _id: 5 } + expectEvents: + - client: *client0 + events: + - *findEvent + - commandStartedEvent: + commandName: insert + command: + insert: *collectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } + + - description: "updateOne includes afterClusterTime in causally consistent session" + operations: + - *findOne + - name: updateOne + object: *collection0 + arguments: + session: *session0 + filter: { _id: 1 } + update: { $set: { x: 100 } } + expectResult: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + expectEvents: + - client: *client0 + events: + - *findEvent + - commandStartedEvent: + commandName: update + command: + update: *collectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } + + - description: "updateMany includes afterClusterTime in causally consistent session" + operations: + - *findOne + - name: updateMany + object: *collection0 + arguments: + session: *session0 + filter: { _id: { $gt: 0 } } + update: { $set: { updated: true } } + expectResult: + matchedCount: 3 + modifiedCount: 3 + upsertedCount: 0 + expectEvents: + - client: *client0 + events: + - *findEvent + - commandStartedEvent: + commandName: update + command: + update: *collectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } + + - description: "replaceOne includes afterClusterTime in causally consistent session" + operations: + - *findOne + - name: replaceOne + object: *collection0 + arguments: + session: *session0 + filter: { _id: 1 } + replacement: { x: 100 } + expectResult: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + expectEvents: + - client: *client0 + events: + - *findEvent + - commandStartedEvent: + commandName: update + command: + update: *collectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } + + - description: "deleteOne includes afterClusterTime in causally consistent session" + operations: + - *findOne + - name: deleteOne + object: *collection0 + arguments: + session: *session0 + filter: { _id: 1 } + expectResult: + deletedCount: 1 + expectEvents: + - client: *client0 + events: + - *findEvent + - commandStartedEvent: + commandName: delete + command: + delete: *collectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } + + - description: "deleteMany includes afterClusterTime in causally consistent session" + operations: + - *findOne + - name: deleteMany + object: *collection0 + arguments: + session: *session0 + filter: { _id: { $gt: 0 } } + expectResult: + deletedCount: 3 + expectEvents: + - client: *client0 + events: + - *findEvent + - commandStartedEvent: + commandName: delete + command: + delete: *collectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } + + - description: "findOneAndUpdate includes afterClusterTime in causally consistent session" + operations: + - *findOne + - name: findOneAndUpdate + object: *collection0 + arguments: + session: *session0 + filter: { _id: 1 } + update: { $set: { x: 100 } } + expectResult: { _id: 1, x: 11 } + expectEvents: + - client: *client0 + events: + - *findEvent + - commandStartedEvent: + commandName: findAndModify + command: + findAndModify: *collectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } + + - description: "findOneAndDelete includes afterClusterTime in causally consistent session" + operations: + - *findOne + - name: findOneAndDelete + object: *collection0 + arguments: + session: *session0 + filter: { _id: 1 } + expectResult: { _id: 1, x: 11 } + expectEvents: + - client: *client0 + events: + - *findEvent + - commandStartedEvent: + commandName: findAndModify + command: + findAndModify: *collectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } + + - description: "findOneAndReplace includes afterClusterTime in causally consistent session" + operations: + - *findOne + - name: findOneAndReplace + object: *collection0 + arguments: + session: *session0 + filter: { _id: 1 } + replacement: { x: 100 } + expectResult: { _id: 1, x: 11 } + expectEvents: + - client: *client0 + events: + - *findEvent + - commandStartedEvent: + commandName: findAndModify + command: + findAndModify: *collectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } + + - description: "bulkWrite includes afterClusterTime in causally consistent session" + operations: + - *findOne + - name: bulkWrite + object: *collection0 + arguments: + session: *session0 + requests: + - insertOne: + document: { _id: 4 } + - updateOne: + filter: { _id: 2 } + update: { $set: { x: 100 } } + - deleteOne: + filter: { _id: 3 } + expectEvents: + - client: *client0 + events: + - *findEvent + - commandStartedEvent: + commandName: insert + command: + insert: *collectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } + - commandStartedEvent: + commandName: update + command: + update: *collectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } + - commandStartedEvent: + commandName: delete + command: + delete: *collectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } diff --git a/source/unified-test-format/tests/Makefile b/source/unified-test-format/tests/Makefile index 4893f267ac..dda5a8ec3e 100644 --- a/source/unified-test-format/tests/Makefile +++ b/source/unified-test-format/tests/Makefile @@ -2,6 +2,7 @@ SCHEMA=../schema-latest.json .PHONY: all \ auth \ + causal-consistency \ change-streams \ client-side-encryption \ client-side-operations-timeout \ @@ -33,6 +34,7 @@ SCHEMA=../schema-latest.json HAS_AJV all: auth \ + causal-consistency \ change-streams \ client-side-encryption \ client-side-operations-timeout \ @@ -62,6 +64,9 @@ all: auth \ auth: HAS_AJV @ajv --spec=draft2019 test -s $(SCHEMA) -d "../../auth/tests/unified/*.yml" --valid +causal-consistency: HAS_AJV + @ajv --spec=draft2019 test -s $(SCHEMA) -d "../../causal-consistency/tests/*.yml" --valid + change-streams: HAS_AJV @ajv --spec=draft2019 test -s $(SCHEMA) -d "../../change-streams/tests/unified/*.yml" --valid From 116399291a9d1a1819ae7a9ec22c2b35830b4f62 Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Mon, 4 May 2026 16:16:53 -0700 Subject: [PATCH 02/11] Update the spec to cover all read and write commands. Add a clientBulkWrite test. --- .../causal-consistency/causal-consistency.md | 38 +++-- .../causal-consistency-clientBulkWrite.json | 146 ++++++++++++++++++ .../causal-consistency-clientBulkWrite.yml | 74 +++++++++ .../causal-consistency-write-commands.json | 5 +- .../causal-consistency-write-commands.yml | 2 + .../read-write-concern/read-write-concern.md | 9 ++ 6 files changed, 261 insertions(+), 13 deletions(-) create mode 100644 source/causal-consistency/tests/causal-consistency-clientBulkWrite.json create mode 100644 source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml diff --git a/source/causal-consistency/causal-consistency.md b/source/causal-consistency/causal-consistency.md index 33c80bc330..bed466e604 100644 --- a/source/causal-consistency/causal-consistency.md +++ b/source/causal-consistency/causal-consistency.md @@ -101,7 +101,7 @@ options = new SessionOptions(causalConsistency = true); session = client.startSession(options); ``` -All read operations performed using this session will now be causally consistent. +All read and write operations performed using this session will now be causally consistent. If no value is provided for `causalConsistency` and snapshot reads are not requested a value of true is implied. See the `causalConsistency` section. @@ -125,7 +125,7 @@ class SessionOptions { In order to support causal consistency a new property named `causalConsistency` is added to `SessionOptions`. Applications set `causalConsistency` when starting a client session to indicate whether they want causal consistency. -All read operations performed using that client session are then causally consistent. +All read and write operations performed using that client session are then causally consistent. Each new member is documented below. @@ -218,8 +218,8 @@ and write operations). ``` The `operationTime` MUST be stored in the `ClientSession` to later be passed as the `afterClusterTime` field of the -`readConcern` field in subsequent read operations. The `operationTime` is returned whether the command succeeded or not -and MUST be stored in either case. +`readConcern` field in subsequent causally consistent read and write operations. The `operationTime` is returned whether +the command succeeded or not and MUST be stored in either case. Drivers MUST examine all responses from the server for the presence of an `operationTime` field and store the value in the `ClientSession`. @@ -237,7 +237,7 @@ For causal consistency the driver MUST send the `operationTime` saved in the `Cl ```typescript { - find : , // or other read command + find : , // or other read or write command ... // the rest of the command parameters readConcern : { @@ -247,8 +247,9 @@ For causal consistency the driver MUST send the `operationTime` saved in the `Cl } ``` -For the lists of commands that support causally consistent reads, see -[ReadConcern](../read-write-concern/read-write-concern.md#read-concern) spec. +For the list of commands that support causally consistent reads, see the +[ReadConcern](../read-write-concern/read-write-concern.md#read-concern) spec. The write commands `insert`, `update`, +`delete`, and `findAndModify` also accept `readConcern.afterClusterTime` when used in causally consistent sessions. The driver MUST merge the `ReadConcern` specified for the operation with the `operationTime` from the `ClientSession` (which goes in the `afterClusterTime` field) to generate the combined `readConcern` to send to the server. If the level @@ -259,15 +260,16 @@ level does not support causal consistency. The Read and Write Concern specification states that when a user has not specified a `ReadConcern` or has specified the server's default `ReadConcern`, drivers MUST omit the `ReadConcern` parameter when sending the command. For causally -consistent reads this requirement is modified to state that when the `ReadConcern` parameter would normally be omitted -drivers MUST send a `ReadConcern` after all because that is how the `afterClusterTime` value is sent to the server. +consistent reads and writes this requirement is modified to state that when the `ReadConcern` parameter would normally +be omitted drivers MUST send a `ReadConcern` after all because that is how the `afterClusterTime` value is sent to the +server. The Read and Write Concern Specification states that drivers MUST NOT add a `readConcern` field to commands that are run using a generic `runCommand` method. The same is true for causal consistency, so commands that are run using `runCommand` MUST NOT have an `afterClusterTime` field added to them. -When executing a causally consistent read, the `afterClusterTime` field MUST be sent when connected to a deployment that -supports cluster times, and MUST NOT be sent when connected to a deployment that does not support cluster times. +When executing a causally consistent operation, the `afterClusterTime` field MUST be sent when connected to a deployment +that supports cluster times, and MUST NOT be sent when connected to a deployment that does not support cluster times. ## Unacknowledged writes @@ -276,7 +278,7 @@ a write. Since unacknowledged writes don't receive a response from the server (o `ClientSession`'s `operationTime` is not updated after an unacknowledged write. That means that a causally consistent read after an unacknowledged write cannot be causally consistent with the unacknowledged write. Rather than prohibiting unacknowledged writes in a causally consistent session we have decided to accept this limitation. Drivers MUST document -that causally consistent reads are not causally consistent with unacknowledged writes. +that causally consistent operations are not causally consistent with unacknowledged writes. ## Test Plan @@ -370,6 +372,15 @@ any deployment that is version 3.6 or higher and is either a replica set or a sh - `document = collection.findOne({})` - capture the command sent to the server - assert that the command includes a `$clusterTime` field +13. A `findOne` followed by any write operation (test them all) should include the `operationTime` returned by the + server for the `findOne` in the `afterClusterTime` parameter of the write operation. + - skip this test if connected to a deployment that does not support cluster times + - `session = client.startSession(causalConsistency = true)` + - `collection.findOne(session, {})` + - `operationTime = session.operationTime` + - `collection.anyWriteOperation(session, ...)` + - capture the command sent to the server (using APM or other mechanism) + - assert that the command has an `afterClusterTime` field with a value of `operationTime` ## Motivation @@ -403,6 +414,9 @@ resolving many discussions of spec details. A final reference implementation mus ## Changelog +- 2026-05-04: Require `afterClusterTime` on write commands (`insert`, `update`, `delete`, `findAndModify`) in causally + consistent sessions, not only on read commands. Added prose test 13. + - 2024-02-08: Migrated from reStructuredText to Markdown. - 2022-11-11: Require `causalConsistency=false` for implicit sessions. diff --git a/source/causal-consistency/tests/causal-consistency-clientBulkWrite.json b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.json new file mode 100644 index 0000000000..e83f537757 --- /dev/null +++ b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.json @@ -0,0 +1,146 @@ +{ + "description": "causal consistency write commands include afterClusterTime", + "schemaVersion": "1.3", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ] + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false, + "uriOptions": { + "retryWrites": false + }, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "causal-consistency-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + }, + { + "session": { + "id": "session0", + "client": "client0", + "sessionOptions": { + "causalConsistency": true + } + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "causal-consistency-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "clientBulkWrite includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "findOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "clientBulkWrite", + "object": "client0", + "arguments": { + "session": "session0", + "models": [ + { + "insertOne": { + "namespace": "causal-consistency-tests.test", + "document": { + "_id": 4 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "command": { + "bulkWrite": 1, + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + } + ] +} diff --git a/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml new file mode 100644 index 0000000000..e908aa872d --- /dev/null +++ b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml @@ -0,0 +1,74 @@ +description: "causal consistency write commands include afterClusterTime" + +schemaVersion: "1.3" + +runOnRequirements: + - minServerVersion: "8.0" + topologies: [replicaset, sharded, load-balanced] + +createEntities: + - client: + id: &client0 client0 + useMultipleMongoses: false + uriOptions: + retryWrites: false + observeEvents: [commandStartedEvent] + - database: + id: &database0 database0 + client: *client0 + databaseName: &databaseName causal-consistency-tests + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collectionName test + - session: + id: &session0 session0 + client: *client0 + sessionOptions: + causalConsistency: true + +initialData: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + +# In a causally consistent session, once an operationTime has been established by a prior +# operation, subsequent write commands MUST include readConcern.afterClusterTime so the +# server can apply the write causally after the previously-observed data. + +tests: + - description: "clientBulkWrite includes afterClusterTime in causally consistent session" + operations: + - name: findOne + object: *collection0 + arguments: + session: *session0 + filter: { _id: 1 } + expectResult: { _id: 1, x: 11 } + - name: clientBulkWrite + object: *client0 + arguments: + session: *session0 + models: + - insertOne: + namespace: causal-consistency-tests.test + document: { _id: 4 } + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + commandName: find + command: + find: *collectionName + readConcern: { $$exists: false } + lsid: { $$sessionLsid: *session0 } + - commandStartedEvent: + commandName: bulkWrite + command: + bulkWrite: 1 + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } diff --git a/source/causal-consistency/tests/causal-consistency-write-commands.json b/source/causal-consistency/tests/causal-consistency-write-commands.json index fa320ded2c..60912a0074 100644 --- a/source/causal-consistency/tests/causal-consistency-write-commands.json +++ b/source/causal-consistency/tests/causal-consistency-write-commands.json @@ -41,7 +41,10 @@ { "session": { "id": "session0", - "client": "client0" + "client": "client0", + "sessionOptions": { + "causalConsistency": true + } } } ], diff --git a/source/causal-consistency/tests/causal-consistency-write-commands.yml b/source/causal-consistency/tests/causal-consistency-write-commands.yml index 5e93894e00..890858a660 100644 --- a/source/causal-consistency/tests/causal-consistency-write-commands.yml +++ b/source/causal-consistency/tests/causal-consistency-write-commands.yml @@ -24,6 +24,8 @@ createEntities: - session: id: &session0 session0 client: *client0 + sessionOptions: + causalConsistency: true initialData: - collectionName: *collectionName diff --git a/source/read-write-concern/read-write-concern.md b/source/read-write-concern/read-write-concern.md index ad779bf2c4..795b801a73 100644 --- a/source/read-write-concern/read-write-concern.md +++ b/source/read-write-concern/read-write-concern.md @@ -78,6 +78,15 @@ The read concern option is available for the following operations: - `parallelCollectionScan` command - `geoNear` command - `geoSearch` command +- `insert` command +- `update` command +- `findAndModify` command +- `delete` command + +> [!NOTE] +> See +> [Operations That Support Read Concern](https://www.mongodb.com/docs/manual/reference/read-concern/#operations-that-support-read-concern) +> for the complete list. Starting in MongoDB 4.2, an `aggregate` command with a write stage (e.g. `$out`, `$merge`) supports a `readConcern`; however, it does not support the "linearizable" level (attempting to do so will result in a server error). From a66b592d7e34ceda76c8dff85a9ce437850b815a Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Mon, 4 May 2026 23:53:39 -0700 Subject: [PATCH 03/11] Remove unnecessary minServerVersion and redundant prose test. Add bulkWrite to the description of write commands. --- source/causal-consistency/causal-consistency.md | 16 ++++------------ .../tests/causal-consistency-write-commands.json | 1 - .../tests/causal-consistency-write-commands.yml | 3 +-- source/read-write-concern/read-write-concern.md | 6 ++++-- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/source/causal-consistency/causal-consistency.md b/source/causal-consistency/causal-consistency.md index bed466e604..f82f3ea888 100644 --- a/source/causal-consistency/causal-consistency.md +++ b/source/causal-consistency/causal-consistency.md @@ -249,7 +249,8 @@ For causal consistency the driver MUST send the `operationTime` saved in the `Cl For the list of commands that support causally consistent reads, see the [ReadConcern](../read-write-concern/read-write-concern.md#read-concern) spec. The write commands `insert`, `update`, -`delete`, and `findAndModify` also accept `readConcern.afterClusterTime` when used in causally consistent sessions. +`delete`, `findAndModify`, and `bulkWrite` also accept `readConcern.afterClusterTime` when used in causally consistent +sessions. The driver MUST merge the `ReadConcern` specified for the operation with the `operationTime` from the `ClientSession` (which goes in the `afterClusterTime` field) to generate the combined `readConcern` to send to the server. If the level @@ -372,15 +373,6 @@ any deployment that is version 3.6 or higher and is either a replica set or a sh - `document = collection.findOne({})` - capture the command sent to the server - assert that the command includes a `$clusterTime` field -13. A `findOne` followed by any write operation (test them all) should include the `operationTime` returned by the - server for the `findOne` in the `afterClusterTime` parameter of the write operation. - - skip this test if connected to a deployment that does not support cluster times - - `session = client.startSession(causalConsistency = true)` - - `collection.findOne(session, {})` - - `operationTime = session.operationTime` - - `collection.anyWriteOperation(session, ...)` - - capture the command sent to the server (using APM or other mechanism) - - assert that the command has an `afterClusterTime` field with a value of `operationTime` ## Motivation @@ -414,8 +406,8 @@ resolving many discussions of spec details. A final reference implementation mus ## Changelog -- 2026-05-04: Require `afterClusterTime` on write commands (`insert`, `update`, `delete`, `findAndModify`) in causally - consistent sessions, not only on read commands. Added prose test 13. +- 2026-05-04: Require `afterClusterTime` on write commands (`insert`, `update`, `delete`, `findAndModify`, `bulkWrite`) + in causally consistent sessions, not only on read commands. Added prose test 13. - 2024-02-08: Migrated from reStructuredText to Markdown. diff --git a/source/causal-consistency/tests/causal-consistency-write-commands.json b/source/causal-consistency/tests/causal-consistency-write-commands.json index 60912a0074..ad8fc1f898 100644 --- a/source/causal-consistency/tests/causal-consistency-write-commands.json +++ b/source/causal-consistency/tests/causal-consistency-write-commands.json @@ -3,7 +3,6 @@ "schemaVersion": "1.3", "runOnRequirements": [ { - "minServerVersion": "3.6", "topologies": [ "replicaset", "sharded", diff --git a/source/causal-consistency/tests/causal-consistency-write-commands.yml b/source/causal-consistency/tests/causal-consistency-write-commands.yml index 890858a660..fee449fd8c 100644 --- a/source/causal-consistency/tests/causal-consistency-write-commands.yml +++ b/source/causal-consistency/tests/causal-consistency-write-commands.yml @@ -3,8 +3,7 @@ description: "causal consistency write commands include afterClusterTime" schemaVersion: "1.3" runOnRequirements: - - minServerVersion: "3.6" - topologies: [replicaset, sharded, load-balanced] + - topologies: [replicaset, sharded, load-balanced] createEntities: - client: diff --git a/source/read-write-concern/read-write-concern.md b/source/read-write-concern/read-write-concern.md index 795b801a73..83f2234963 100644 --- a/source/read-write-concern/read-write-concern.md +++ b/source/read-write-concern/read-write-concern.md @@ -68,7 +68,7 @@ class ReadConcern { } ``` -The read concern option is available for the following operations: +The read concern option is available for most operations, including the following: - `aggregate` command - `count` command @@ -82,11 +82,13 @@ The read concern option is available for the following operations: - `update` command - `findAndModify` command - `delete` command +- `bulkWrite` command > [!NOTE] > See > [Operations That Support Read Concern](https://www.mongodb.com/docs/manual/reference/read-concern/#operations-that-support-read-concern) -> for the complete list. +> for the complete list of operations that support read concern. Note that `bulkWrite` is erroneously omitted from that +> list. Starting in MongoDB 4.2, an `aggregate` command with a write stage (e.g. `$out`, `$merge`) supports a `readConcern`; however, it does not support the "linearizable" level (attempting to do so will result in a server error). From 2994ef4252d9aad69ac3c430a82b1b81096d8038 Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Tue, 5 May 2026 20:49:27 -0700 Subject: [PATCH 04/11] Expect DDL operations to send afterClusterTime. --- .../causal-consistency/causal-consistency.md | 8 +- .../causal-consistency-write-commands.json | 258 ++++++++++++++++++ .../causal-consistency-write-commands.yml | 80 ++++++ .../read-write-concern/read-write-concern.md | 5 + 4 files changed, 346 insertions(+), 5 deletions(-) diff --git a/source/causal-consistency/causal-consistency.md b/source/causal-consistency/causal-consistency.md index f82f3ea888..c0e4e81f3b 100644 --- a/source/causal-consistency/causal-consistency.md +++ b/source/causal-consistency/causal-consistency.md @@ -248,9 +248,7 @@ For causal consistency the driver MUST send the `operationTime` saved in the `Cl ``` For the list of commands that support causally consistent reads, see the -[ReadConcern](../read-write-concern/read-write-concern.md#read-concern) spec. The write commands `insert`, `update`, -`delete`, `findAndModify`, and `bulkWrite` also accept `readConcern.afterClusterTime` when used in causally consistent -sessions. +[ReadConcern](../read-write-concern/read-write-concern.md#read-concern) spec. The driver MUST merge the `ReadConcern` specified for the operation with the `operationTime` from the `ClientSession` (which goes in the `afterClusterTime` field) to generate the combined `readConcern` to send to the server. If the level @@ -406,8 +404,8 @@ resolving many discussions of spec details. A final reference implementation mus ## Changelog -- 2026-05-04: Require `afterClusterTime` on write commands (`insert`, `update`, `delete`, `findAndModify`, `bulkWrite`) - in causally consistent sessions, not only on read commands. Added prose test 13. +- 2026-05-04: Require `afterClusterTime` on all write commands in causally-consistent sessions, not only on read + commands. - 2024-02-08: Migrated from reStructuredText to Markdown. diff --git a/source/causal-consistency/tests/causal-consistency-write-commands.json b/source/causal-consistency/tests/causal-consistency-write-commands.json index ad8fc1f898..f99582a3d1 100644 --- a/source/causal-consistency/tests/causal-consistency-write-commands.json +++ b/source/causal-consistency/tests/causal-consistency-write-commands.json @@ -920,6 +920,264 @@ ] } ] + }, + { + "description": "create includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "findOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "createCollection", + "object": "database0", + "arguments": { + "session": "session0", + "collection": "newcoll" + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "create", + "command": { + "create": "newcoll", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "createIndexes includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "findOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "createIndex", + "object": "collection0", + "arguments": { + "session": "session0", + "keys": { + "x": 1 + }, + "name": "x_1" + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "createIndexes", + "command": { + "createIndexes": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "drop includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "findOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "dropCollection", + "object": "database0", + "arguments": { + "session": "session0", + "collection": "test" + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "drop", + "command": { + "drop": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] + }, + { + "description": "dropIndexes includes afterClusterTime in causally consistent session", + "operations": [ + { + "name": "findOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "expectResult": { + "_id": 1, + "x": 11 + } + }, + { + "name": "dropIndexes", + "object": "collection0", + "arguments": { + "session": "session0" + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "find", + "command": { + "find": "test", + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "dropIndexes", + "command": { + "dropIndexes": "test", + "lsid": { + "$$sessionLsid": "session0" + }, + "readConcern": { + "afterClusterTime": { + "$$exists": true + } + } + } + } + } + ] + } + ] } ] } diff --git a/source/causal-consistency/tests/causal-consistency-write-commands.yml b/source/causal-consistency/tests/causal-consistency-write-commands.yml index fee449fd8c..7cbc19c702 100644 --- a/source/causal-consistency/tests/causal-consistency-write-commands.yml +++ b/source/causal-consistency/tests/causal-consistency-write-commands.yml @@ -319,3 +319,83 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + + - description: "create includes afterClusterTime in causally consistent session" + operations: + - *findOne + - name: createCollection + object: *database0 + arguments: + session: *session0 + collection: &newCollectionName newcoll + expectEvents: + - client: *client0 + events: + - *findEvent + - commandStartedEvent: + commandName: create + command: + create: *newCollectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } + + - description: "createIndexes includes afterClusterTime in causally consistent session" + operations: + - *findOne + - name: createIndex + object: *collection0 + arguments: + session: *session0 + keys: { x: 1 } + name: x_1 + expectEvents: + - client: *client0 + events: + - *findEvent + - commandStartedEvent: + commandName: createIndexes + command: + createIndexes: *collectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } + + - description: "drop includes afterClusterTime in causally consistent session" + operations: + - *findOne + - name: dropCollection + object: *database0 + arguments: + session: *session0 + collection: *collectionName + expectEvents: + - client: *client0 + events: + - *findEvent + - commandStartedEvent: + commandName: drop + command: + drop: *collectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } + + - description: "dropIndexes includes afterClusterTime in causally consistent session" + operations: + - *findOne + - name: dropIndexes + object: *collection0 + arguments: + session: *session0 + expectEvents: + - client: *client0 + events: + - *findEvent + - commandStartedEvent: + commandName: dropIndexes + command: + dropIndexes: *collectionName + lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } diff --git a/source/read-write-concern/read-write-concern.md b/source/read-write-concern/read-write-concern.md index 83f2234963..3cf266fb7b 100644 --- a/source/read-write-concern/read-write-concern.md +++ b/source/read-write-concern/read-write-concern.md @@ -83,6 +83,11 @@ The read concern option is available for most operations, including the followin - `findAndModify` command - `delete` command - `bulkWrite` command +- `create` command +- `createIndexes` command +- `drop` command +- `dropDatabase` command +- `dropIndexes` command > [!NOTE] > See From c4c9860d31dae1608cb3dd2dad48e20f71f6d73f Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Thu, 7 May 2026 00:54:55 -0700 Subject: [PATCH 05/11] Explicitly forbid 'level' in 'readConcern' for writes. Fix transactions test assertions. --- .../causal-consistency-clientBulkWrite.json | 3 ++ .../causal-consistency-clientBulkWrite.yml | 1 + .../causal-consistency-write-commands.json | 51 +++++++++++++++++++ .../causal-consistency-write-commands.yml | 17 +++++++ .../tests/unified/callback-aborts.json | 4 +- .../tests/unified/callback-aborts.yml | 3 +- .../tests/unified/callback-commits.json | 4 +- .../tests/unified/callback-commits.yml | 4 +- source/transactions/tests/unified/commit.json | 8 ++- source/transactions/tests/unified/commit.yml | 6 ++- .../tests/unified/retryable-writes.json | 8 ++- .../tests/unified/retryable-writes.yml | 6 ++- 12 files changed, 102 insertions(+), 13 deletions(-) diff --git a/source/causal-consistency/tests/causal-consistency-clientBulkWrite.json b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.json index e83f537757..01182f12c5 100644 --- a/source/causal-consistency/tests/causal-consistency-clientBulkWrite.json +++ b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.json @@ -133,6 +133,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } diff --git a/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml index e908aa872d..a510d59165 100644 --- a/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml +++ b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml @@ -72,3 +72,4 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } diff --git a/source/causal-consistency/tests/causal-consistency-write-commands.json b/source/causal-consistency/tests/causal-consistency-write-commands.json index f99582a3d1..b0fc550049 100644 --- a/source/causal-consistency/tests/causal-consistency-write-commands.json +++ b/source/causal-consistency/tests/causal-consistency-write-commands.json @@ -132,6 +132,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } @@ -203,6 +206,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } @@ -279,6 +285,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } @@ -357,6 +366,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } @@ -431,6 +443,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } @@ -500,6 +515,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } @@ -571,6 +589,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } @@ -646,6 +667,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } @@ -716,6 +740,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } @@ -789,6 +816,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } @@ -880,6 +910,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } @@ -896,6 +929,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } @@ -912,6 +948,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } @@ -976,6 +1015,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } @@ -1043,6 +1085,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } @@ -1107,6 +1152,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } @@ -1170,6 +1218,9 @@ "readConcern": { "afterClusterTime": { "$$exists": true + }, + "level": { + "$$exists": false } } } diff --git a/source/causal-consistency/tests/causal-consistency-write-commands.yml b/source/causal-consistency/tests/causal-consistency-write-commands.yml index 7cbc19c702..7176aaf79a 100644 --- a/source/causal-consistency/tests/causal-consistency-write-commands.yml +++ b/source/causal-consistency/tests/causal-consistency-write-commands.yml @@ -72,6 +72,7 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } - description: "insertMany includes afterClusterTime in causally consistent session" operations: @@ -94,6 +95,7 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } - description: "updateOne includes afterClusterTime in causally consistent session" operations: @@ -119,6 +121,7 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } - description: "updateMany includes afterClusterTime in causally consistent session" operations: @@ -144,6 +147,7 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } - description: "replaceOne includes afterClusterTime in causally consistent session" operations: @@ -169,6 +173,7 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } - description: "deleteOne includes afterClusterTime in causally consistent session" operations: @@ -191,6 +196,7 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } - description: "deleteMany includes afterClusterTime in causally consistent session" operations: @@ -213,6 +219,7 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } - description: "findOneAndUpdate includes afterClusterTime in causally consistent session" operations: @@ -235,6 +242,7 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } - description: "findOneAndDelete includes afterClusterTime in causally consistent session" operations: @@ -256,6 +264,7 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } - description: "findOneAndReplace includes afterClusterTime in causally consistent session" operations: @@ -278,6 +287,7 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } - description: "bulkWrite includes afterClusterTime in causally consistent session" operations: @@ -305,6 +315,7 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } - commandStartedEvent: commandName: update command: @@ -312,6 +323,7 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } - commandStartedEvent: commandName: delete command: @@ -319,6 +331,7 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } - description: "create includes afterClusterTime in causally consistent session" operations: @@ -339,6 +352,7 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } - description: "createIndexes includes afterClusterTime in causally consistent session" operations: @@ -360,6 +374,7 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } - description: "drop includes afterClusterTime in causally consistent session" operations: @@ -380,6 +395,7 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } - description: "dropIndexes includes afterClusterTime in causally consistent session" operations: @@ -399,3 +415,4 @@ tests: lsid: { $$sessionLsid: *session0 } readConcern: afterClusterTime: { $$exists: true } + level: { $$exists: false } diff --git a/source/transactions-convenient-api/tests/unified/callback-aborts.json b/source/transactions-convenient-api/tests/unified/callback-aborts.json index 206428715c..dfe7cdcd5b 100644 --- a/source/transactions-convenient-api/tests/unified/callback-aborts.json +++ b/source/transactions-convenient-api/tests/unified/callback-aborts.json @@ -260,7 +260,9 @@ "startTransaction": true, "autocommit": false, "readConcern": { - "$$exists": false + "afterClusterTime": { + "$$exists": true + } }, "writeConcern": { "$$exists": false diff --git a/source/transactions-convenient-api/tests/unified/callback-aborts.yml b/source/transactions-convenient-api/tests/unified/callback-aborts.yml index 9414040eca..7d9013dcaa 100644 --- a/source/transactions-convenient-api/tests/unified/callback-aborts.yml +++ b/source/transactions-convenient-api/tests/unified/callback-aborts.yml @@ -136,8 +136,9 @@ tests: txnNumber: { $numberLong: "1" } startTransaction: true autocommit: false + readConcern: + afterClusterTime: { $$exists: true } # omitted fields - readConcern: { $$exists: false } writeConcern: { $$exists: false } commandName: insert databaseName: *databaseName diff --git a/source/transactions-convenient-api/tests/unified/callback-commits.json b/source/transactions-convenient-api/tests/unified/callback-commits.json index 06f791e9ae..d239ab0ca2 100644 --- a/source/transactions-convenient-api/tests/unified/callback-commits.json +++ b/source/transactions-convenient-api/tests/unified/callback-commits.json @@ -302,7 +302,9 @@ "startTransaction": true, "autocommit": false, "readConcern": { - "$$exists": false + "afterClusterTime": { + "$$exists": true + } }, "writeConcern": { "$$exists": false diff --git a/source/transactions-convenient-api/tests/unified/callback-commits.yml b/source/transactions-convenient-api/tests/unified/callback-commits.yml index b5cbb04151..56d810458d 100644 --- a/source/transactions-convenient-api/tests/unified/callback-commits.yml +++ b/source/transactions-convenient-api/tests/unified/callback-commits.yml @@ -150,8 +150,8 @@ tests: txnNumber: { $numberLong: "1" } startTransaction: true autocommit: false - # omitted fields - readConcern: { $$exists: false } + readConcern: + afterClusterTime: { $$exists: true } writeConcern: { $$exists: false } commandName: insert databaseName: *databaseName diff --git a/source/transactions/tests/unified/commit.json b/source/transactions/tests/unified/commit.json index ab778d8df2..f033906940 100644 --- a/source/transactions/tests/unified/commit.json +++ b/source/transactions/tests/unified/commit.json @@ -1040,7 +1040,9 @@ ], "ordered": true, "readConcern": { - "$$exists": false + "afterClusterTime": { + "$$exists": true + } }, "lsid": { "$$sessionLsid": "session1" @@ -1196,7 +1198,9 @@ ], "ordered": true, "readConcern": { - "$$exists": false + "afterClusterTime": { + "$$exists": true + } }, "lsid": { "$$sessionLsid": "session1" diff --git a/source/transactions/tests/unified/commit.yml b/source/transactions/tests/unified/commit.yml index d9af084894..eaf4ee39ba 100644 --- a/source/transactions/tests/unified/commit.yml +++ b/source/transactions/tests/unified/commit.yml @@ -615,7 +615,8 @@ tests: documents: - { _id: 2 } ordered: true - readConcern: { $$exists: false } + readConcern: + afterClusterTime: { $$exists: true } lsid: { $$sessionLsid: *session1 } txnNumber: { $$exists: false } startTransaction: { $$exists: false } @@ -698,7 +699,8 @@ tests: documents: - { _id: 2 } ordered: true - readConcern: { $$exists: false } + readConcern: + afterClusterTime: { $$exists: true } lsid: { $$sessionLsid: *session1 } txnNumber: { $$exists: false } startTransaction: { $$exists: false } diff --git a/source/transactions/tests/unified/retryable-writes.json b/source/transactions/tests/unified/retryable-writes.json index c196e68622..21ef22b8ab 100644 --- a/source/transactions/tests/unified/retryable-writes.json +++ b/source/transactions/tests/unified/retryable-writes.json @@ -217,7 +217,9 @@ ], "ordered": true, "readConcern": { - "$$exists": false + "afterClusterTime": { + "$$exists": true + } }, "lsid": { "$$sessionLsid": "session0" @@ -306,7 +308,9 @@ ], "ordered": true, "readConcern": { - "$$exists": false + "afterClusterTime": { + "$$exists": true + } }, "lsid": { "$$sessionLsid": "session0" diff --git a/source/transactions/tests/unified/retryable-writes.yml b/source/transactions/tests/unified/retryable-writes.yml index aa9c037d41..81339c9ad9 100644 --- a/source/transactions/tests/unified/retryable-writes.yml +++ b/source/transactions/tests/unified/retryable-writes.yml @@ -132,7 +132,8 @@ tests: documents: - { _id: 2 } ordered: true - readConcern: { $$exists: false } + readConcern: + afterClusterTime: { $$exists: true } lsid: { $$sessionLsid: *session0 } txnNumber: { $numberLong: '2' } startTransaction: { $$exists: false } @@ -175,7 +176,8 @@ tests: - { _id: 4 } - { _id: 5 } ordered: true - readConcern: { $$exists: false } + readConcern: + afterClusterTime: { $$exists: true } lsid: { $$sessionLsid: *session0 } txnNumber: { $numberLong: '4' } startTransaction: { $$exists: false } From 51c492009a87b8162b31da1fd75af4b2c11cdb54 Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Thu, 7 May 2026 12:16:47 -0700 Subject: [PATCH 06/11] Replace findOne with find for the initial operation. --- .../causal-consistency-clientBulkWrite.json | 12 +- .../causal-consistency-clientBulkWrite.yml | 4 +- .../causal-consistency-write-commands.json | 180 ++++++++++-------- .../causal-consistency-write-commands.yml | 34 ++-- 4 files changed, 131 insertions(+), 99 deletions(-) diff --git a/source/causal-consistency/tests/causal-consistency-clientBulkWrite.json b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.json index 01182f12c5..8dd201d191 100644 --- a/source/causal-consistency/tests/causal-consistency-clientBulkWrite.json +++ b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.json @@ -73,7 +73,7 @@ "description": "clientBulkWrite includes afterClusterTime in causally consistent session", "operations": [ { - "name": "findOne", + "name": "find", "object": "collection0", "arguments": { "session": "session0", @@ -81,10 +81,12 @@ "_id": 1 } }, - "expectResult": { - "_id": 1, - "x": 11 - } + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] }, { "name": "clientBulkWrite", diff --git a/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml index a510d59165..be36281abd 100644 --- a/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml +++ b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml @@ -42,12 +42,12 @@ initialData: tests: - description: "clientBulkWrite includes afterClusterTime in causally consistent session" operations: - - name: findOne + - name: find object: *collection0 arguments: session: *session0 filter: { _id: 1 } - expectResult: { _id: 1, x: 11 } + expectResult: [{ _id: 1, x: 11 }] - name: clientBulkWrite object: *client0 arguments: diff --git a/source/causal-consistency/tests/causal-consistency-write-commands.json b/source/causal-consistency/tests/causal-consistency-write-commands.json index b0fc550049..642bbdcc7a 100644 --- a/source/causal-consistency/tests/causal-consistency-write-commands.json +++ b/source/causal-consistency/tests/causal-consistency-write-commands.json @@ -72,7 +72,7 @@ "description": "insertOne includes afterClusterTime in causally consistent session", "operations": [ { - "name": "findOne", + "name": "find", "object": "collection0", "arguments": { "session": "session0", @@ -80,10 +80,12 @@ "_id": 1 } }, - "expectResult": { - "_id": 1, - "x": 11 - } + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] }, { "name": "insertOne", @@ -148,7 +150,7 @@ "description": "insertMany includes afterClusterTime in causally consistent session", "operations": [ { - "name": "findOne", + "name": "find", "object": "collection0", "arguments": { "session": "session0", @@ -156,10 +158,12 @@ "_id": 1 } }, - "expectResult": { - "_id": 1, - "x": 11 - } + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] }, { "name": "insertMany", @@ -222,7 +226,7 @@ "description": "updateOne includes afterClusterTime in causally consistent session", "operations": [ { - "name": "findOne", + "name": "find", "object": "collection0", "arguments": { "session": "session0", @@ -230,10 +234,12 @@ "_id": 1 } }, - "expectResult": { - "_id": 1, - "x": 11 - } + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] }, { "name": "updateOne", @@ -301,7 +307,7 @@ "description": "updateMany includes afterClusterTime in causally consistent session", "operations": [ { - "name": "findOne", + "name": "find", "object": "collection0", "arguments": { "session": "session0", @@ -309,10 +315,12 @@ "_id": 1 } }, - "expectResult": { - "_id": 1, - "x": 11 - } + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] }, { "name": "updateMany", @@ -382,7 +390,7 @@ "description": "replaceOne includes afterClusterTime in causally consistent session", "operations": [ { - "name": "findOne", + "name": "find", "object": "collection0", "arguments": { "session": "session0", @@ -390,10 +398,12 @@ "_id": 1 } }, - "expectResult": { - "_id": 1, - "x": 11 - } + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] }, { "name": "replaceOne", @@ -459,7 +469,7 @@ "description": "deleteOne includes afterClusterTime in causally consistent session", "operations": [ { - "name": "findOne", + "name": "find", "object": "collection0", "arguments": { "session": "session0", @@ -467,10 +477,12 @@ "_id": 1 } }, - "expectResult": { - "_id": 1, - "x": 11 - } + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] }, { "name": "deleteOne", @@ -531,7 +543,7 @@ "description": "deleteMany includes afterClusterTime in causally consistent session", "operations": [ { - "name": "findOne", + "name": "find", "object": "collection0", "arguments": { "session": "session0", @@ -539,10 +551,12 @@ "_id": 1 } }, - "expectResult": { - "_id": 1, - "x": 11 - } + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] }, { "name": "deleteMany", @@ -605,7 +619,7 @@ "description": "findOneAndUpdate includes afterClusterTime in causally consistent session", "operations": [ { - "name": "findOne", + "name": "find", "object": "collection0", "arguments": { "session": "session0", @@ -613,10 +627,12 @@ "_id": 1 } }, - "expectResult": { - "_id": 1, - "x": 11 - } + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] }, { "name": "findOneAndUpdate", @@ -683,7 +699,7 @@ "description": "findOneAndDelete includes afterClusterTime in causally consistent session", "operations": [ { - "name": "findOne", + "name": "find", "object": "collection0", "arguments": { "session": "session0", @@ -691,10 +707,12 @@ "_id": 1 } }, - "expectResult": { - "_id": 1, - "x": 11 - } + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] }, { "name": "findOneAndDelete", @@ -756,7 +774,7 @@ "description": "findOneAndReplace includes afterClusterTime in causally consistent session", "operations": [ { - "name": "findOne", + "name": "find", "object": "collection0", "arguments": { "session": "session0", @@ -764,10 +782,12 @@ "_id": 1 } }, - "expectResult": { - "_id": 1, - "x": 11 - } + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] }, { "name": "findOneAndReplace", @@ -832,7 +852,7 @@ "description": "bulkWrite includes afterClusterTime in causally consistent session", "operations": [ { - "name": "findOne", + "name": "find", "object": "collection0", "arguments": { "session": "session0", @@ -840,10 +860,12 @@ "_id": 1 } }, - "expectResult": { - "_id": 1, - "x": 11 - } + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] }, { "name": "bulkWrite", @@ -964,7 +986,7 @@ "description": "create includes afterClusterTime in causally consistent session", "operations": [ { - "name": "findOne", + "name": "find", "object": "collection0", "arguments": { "session": "session0", @@ -972,10 +994,12 @@ "_id": 1 } }, - "expectResult": { - "_id": 1, - "x": 11 - } + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] }, { "name": "createCollection", @@ -1031,7 +1055,7 @@ "description": "createIndexes includes afterClusterTime in causally consistent session", "operations": [ { - "name": "findOne", + "name": "find", "object": "collection0", "arguments": { "session": "session0", @@ -1039,10 +1063,12 @@ "_id": 1 } }, - "expectResult": { - "_id": 1, - "x": 11 - } + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] }, { "name": "createIndex", @@ -1101,7 +1127,7 @@ "description": "drop includes afterClusterTime in causally consistent session", "operations": [ { - "name": "findOne", + "name": "find", "object": "collection0", "arguments": { "session": "session0", @@ -1109,10 +1135,12 @@ "_id": 1 } }, - "expectResult": { - "_id": 1, - "x": 11 - } + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] }, { "name": "dropCollection", @@ -1168,7 +1196,7 @@ "description": "dropIndexes includes afterClusterTime in causally consistent session", "operations": [ { - "name": "findOne", + "name": "find", "object": "collection0", "arguments": { "session": "session0", @@ -1176,10 +1204,12 @@ "_id": 1 } }, - "expectResult": { - "_id": 1, - "x": 11 - } + "expectResult": [ + { + "_id": 1, + "x": 11 + } + ] }, { "name": "dropIndexes", diff --git a/source/causal-consistency/tests/causal-consistency-write-commands.yml b/source/causal-consistency/tests/causal-consistency-write-commands.yml index 7176aaf79a..1af2c63bfe 100644 --- a/source/causal-consistency/tests/causal-consistency-write-commands.yml +++ b/source/causal-consistency/tests/causal-consistency-write-commands.yml @@ -41,13 +41,13 @@ initialData: tests: - description: "insertOne includes afterClusterTime in causally consistent session" operations: - - &findOne - name: findOne + - &find + name: find object: *collection0 arguments: session: *session0 filter: { _id: 1 } - expectResult: { _id: 1, x: 11 } + expectResult: [{ _id: 1, x: 11 }] - name: insertOne object: *collection0 arguments: @@ -76,7 +76,7 @@ tests: - description: "insertMany includes afterClusterTime in causally consistent session" operations: - - *findOne + - *find - name: insertMany object: *collection0 arguments: @@ -99,7 +99,7 @@ tests: - description: "updateOne includes afterClusterTime in causally consistent session" operations: - - *findOne + - *find - name: updateOne object: *collection0 arguments: @@ -125,7 +125,7 @@ tests: - description: "updateMany includes afterClusterTime in causally consistent session" operations: - - *findOne + - *find - name: updateMany object: *collection0 arguments: @@ -151,7 +151,7 @@ tests: - description: "replaceOne includes afterClusterTime in causally consistent session" operations: - - *findOne + - *find - name: replaceOne object: *collection0 arguments: @@ -177,7 +177,7 @@ tests: - description: "deleteOne includes afterClusterTime in causally consistent session" operations: - - *findOne + - *find - name: deleteOne object: *collection0 arguments: @@ -200,7 +200,7 @@ tests: - description: "deleteMany includes afterClusterTime in causally consistent session" operations: - - *findOne + - *find - name: deleteMany object: *collection0 arguments: @@ -223,7 +223,7 @@ tests: - description: "findOneAndUpdate includes afterClusterTime in causally consistent session" operations: - - *findOne + - *find - name: findOneAndUpdate object: *collection0 arguments: @@ -246,7 +246,7 @@ tests: - description: "findOneAndDelete includes afterClusterTime in causally consistent session" operations: - - *findOne + - *find - name: findOneAndDelete object: *collection0 arguments: @@ -268,7 +268,7 @@ tests: - description: "findOneAndReplace includes afterClusterTime in causally consistent session" operations: - - *findOne + - *find - name: findOneAndReplace object: *collection0 arguments: @@ -291,7 +291,7 @@ tests: - description: "bulkWrite includes afterClusterTime in causally consistent session" operations: - - *findOne + - *find - name: bulkWrite object: *collection0 arguments: @@ -335,7 +335,7 @@ tests: - description: "create includes afterClusterTime in causally consistent session" operations: - - *findOne + - *find - name: createCollection object: *database0 arguments: @@ -356,7 +356,7 @@ tests: - description: "createIndexes includes afterClusterTime in causally consistent session" operations: - - *findOne + - *find - name: createIndex object: *collection0 arguments: @@ -378,7 +378,7 @@ tests: - description: "drop includes afterClusterTime in causally consistent session" operations: - - *findOne + - *find - name: dropCollection object: *database0 arguments: @@ -399,7 +399,7 @@ tests: - description: "dropIndexes includes afterClusterTime in causally consistent session" operations: - - *findOne + - *find - name: dropIndexes object: *collection0 arguments: From 10bb34b9735f4581f75f6b9a04613f66bb685f48 Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Thu, 7 May 2026 16:29:31 -0700 Subject: [PATCH 07/11] Writes in transactions don't require afterClusterTime. --- .../tests/unified/callback-aborts.json | 4 +--- .../tests/unified/callback-aborts.yml | 3 +-- .../tests/unified/callback-commits.json | 4 +--- .../tests/unified/callback-commits.yml | 4 ++-- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/source/transactions-convenient-api/tests/unified/callback-aborts.json b/source/transactions-convenient-api/tests/unified/callback-aborts.json index dfe7cdcd5b..206428715c 100644 --- a/source/transactions-convenient-api/tests/unified/callback-aborts.json +++ b/source/transactions-convenient-api/tests/unified/callback-aborts.json @@ -260,9 +260,7 @@ "startTransaction": true, "autocommit": false, "readConcern": { - "afterClusterTime": { - "$$exists": true - } + "$$exists": false }, "writeConcern": { "$$exists": false diff --git a/source/transactions-convenient-api/tests/unified/callback-aborts.yml b/source/transactions-convenient-api/tests/unified/callback-aborts.yml index 7d9013dcaa..9414040eca 100644 --- a/source/transactions-convenient-api/tests/unified/callback-aborts.yml +++ b/source/transactions-convenient-api/tests/unified/callback-aborts.yml @@ -136,9 +136,8 @@ tests: txnNumber: { $numberLong: "1" } startTransaction: true autocommit: false - readConcern: - afterClusterTime: { $$exists: true } # omitted fields + readConcern: { $$exists: false } writeConcern: { $$exists: false } commandName: insert databaseName: *databaseName diff --git a/source/transactions-convenient-api/tests/unified/callback-commits.json b/source/transactions-convenient-api/tests/unified/callback-commits.json index d239ab0ca2..06f791e9ae 100644 --- a/source/transactions-convenient-api/tests/unified/callback-commits.json +++ b/source/transactions-convenient-api/tests/unified/callback-commits.json @@ -302,9 +302,7 @@ "startTransaction": true, "autocommit": false, "readConcern": { - "afterClusterTime": { - "$$exists": true - } + "$$exists": false }, "writeConcern": { "$$exists": false diff --git a/source/transactions-convenient-api/tests/unified/callback-commits.yml b/source/transactions-convenient-api/tests/unified/callback-commits.yml index 56d810458d..b5cbb04151 100644 --- a/source/transactions-convenient-api/tests/unified/callback-commits.yml +++ b/source/transactions-convenient-api/tests/unified/callback-commits.yml @@ -150,8 +150,8 @@ tests: txnNumber: { $numberLong: "1" } startTransaction: true autocommit: false - readConcern: - afterClusterTime: { $$exists: true } + # omitted fields + readConcern: { $$exists: false } writeConcern: { $$exists: false } commandName: insert databaseName: *databaseName From c5b9d047595a5e5bc628eb44ef225398c4a9cd67 Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Thu, 7 May 2026 16:44:18 -0700 Subject: [PATCH 08/11] Require afterClusterTime on writes not in a transaction. --- .../tests/unified/callback-aborts.json | 8 +++++--- .../tests/unified/callback-aborts.yml | 3 ++- .../tests/unified/callback-commits.json | 8 +++++--- .../tests/unified/callback-commits.yml | 3 ++- 4 files changed, 14 insertions(+), 8 deletions(-) diff --git a/source/transactions-convenient-api/tests/unified/callback-aborts.json b/source/transactions-convenient-api/tests/unified/callback-aborts.json index 206428715c..dc8f7fb5a4 100644 --- a/source/transactions-convenient-api/tests/unified/callback-aborts.json +++ b/source/transactions-convenient-api/tests/unified/callback-aborts.json @@ -308,10 +308,12 @@ "lsid": { "$$sessionLsid": "session0" }, - "autocommit": { - "$$exists": false - }, "readConcern": { + "afterClusterTime": { + "$$exists": true + } + }, + "autocommit": { "$$exists": false }, "startTransaction": { diff --git a/source/transactions-convenient-api/tests/unified/callback-aborts.yml b/source/transactions-convenient-api/tests/unified/callback-aborts.yml index 9414040eca..d5a95d7776 100644 --- a/source/transactions-convenient-api/tests/unified/callback-aborts.yml +++ b/source/transactions-convenient-api/tests/unified/callback-aborts.yml @@ -164,9 +164,10 @@ tests: - { _id: 2 } ordered: true lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } # omitted fields autocommit: { $$exists: false } - readConcern: { $$exists: false } startTransaction: { $$exists: false } writeConcern: { $$exists: false } commandName: insert diff --git a/source/transactions-convenient-api/tests/unified/callback-commits.json b/source/transactions-convenient-api/tests/unified/callback-commits.json index 06f791e9ae..edda386418 100644 --- a/source/transactions-convenient-api/tests/unified/callback-commits.json +++ b/source/transactions-convenient-api/tests/unified/callback-commits.json @@ -381,10 +381,12 @@ "lsid": { "$$sessionLsid": "session0" }, - "autocommit": { - "$$exists": false - }, "readConcern": { + "afterClusterTime": { + "$$exists": true + } + }, + "autocommit": { "$$exists": false }, "startTransaction": { diff --git a/source/transactions-convenient-api/tests/unified/callback-commits.yml b/source/transactions-convenient-api/tests/unified/callback-commits.yml index b5cbb04151..7bd3e7feae 100644 --- a/source/transactions-convenient-api/tests/unified/callback-commits.yml +++ b/source/transactions-convenient-api/tests/unified/callback-commits.yml @@ -193,9 +193,10 @@ tests: - { _id: 3 } ordered: true lsid: { $$sessionLsid: *session0 } + readConcern: + afterClusterTime: { $$exists: true } # omitted fields autocommit: { $$exists: false } - readConcern: { $$exists: false } startTransaction: { $$exists: false } writeConcern: { $$exists: false } commandName: insert From c3033c43985a5dcae02b673c05f5cf46b7fa1937 Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Thu, 7 May 2026 23:23:19 -0700 Subject: [PATCH 09/11] Remove confusing note about commands supporting readConcern in Read Write Concern. --- source/read-write-concern/read-write-concern.md | 6 ------ 1 file changed, 6 deletions(-) diff --git a/source/read-write-concern/read-write-concern.md b/source/read-write-concern/read-write-concern.md index 3cf266fb7b..4455b5fa5b 100644 --- a/source/read-write-concern/read-write-concern.md +++ b/source/read-write-concern/read-write-concern.md @@ -89,12 +89,6 @@ The read concern option is available for most operations, including the followin - `dropDatabase` command - `dropIndexes` command -> [!NOTE] -> See -> [Operations That Support Read Concern](https://www.mongodb.com/docs/manual/reference/read-concern/#operations-that-support-read-concern) -> for the complete list of operations that support read concern. Note that `bulkWrite` is erroneously omitted from that -> list. - Starting in MongoDB 4.2, an `aggregate` command with a write stage (e.g. `$out`, `$merge`) supports a `readConcern`; however, it does not support the "linearizable" level (attempting to do so will result in a server error). From 32f3817ee597a124ec57a16313e633cbc9501e1e Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Fri, 8 May 2026 10:59:00 -0700 Subject: [PATCH 10/11] Make sure there are no collection name conflicts in createCollection test. --- .../causal-consistency-write-commands.json | 23 +++++++++++++++++-- .../causal-consistency-write-commands.yml | 14 ++++++++++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/source/causal-consistency/tests/causal-consistency-write-commands.json b/source/causal-consistency/tests/causal-consistency-write-commands.json index 642bbdcc7a..30460f5912 100644 --- a/source/causal-consistency/tests/causal-consistency-write-commands.json +++ b/source/causal-consistency/tests/causal-consistency-write-commands.json @@ -1001,12 +1001,20 @@ } ] }, + { + "name": "dropCollection", + "object": "database0", + "arguments": { + "session": "session0", + "collection": "causal-consistency-createCollection-test" + } + }, { "name": "createCollection", "object": "database0", "arguments": { "session": "session0", - "collection": "newcoll" + "collection": "causal-consistency-createCollection-test" } } ], @@ -1028,11 +1036,22 @@ } } }, + { + "commandStartedEvent": { + "commandName": "drop", + "command": { + "drop": "causal-consistency-createCollection-test", + "lsid": { + "$$sessionLsid": "session0" + } + } + } + }, { "commandStartedEvent": { "commandName": "create", "command": { - "create": "newcoll", + "create": "causal-consistency-createCollection-test", "lsid": { "$$sessionLsid": "session0" }, diff --git a/source/causal-consistency/tests/causal-consistency-write-commands.yml b/source/causal-consistency/tests/causal-consistency-write-commands.yml index 1af2c63bfe..ffe2d23c4e 100644 --- a/source/causal-consistency/tests/causal-consistency-write-commands.yml +++ b/source/causal-consistency/tests/causal-consistency-write-commands.yml @@ -336,15 +336,27 @@ tests: - description: "create includes afterClusterTime in causally consistent session" operations: - *find + # Drop the collection first to make sure there's no name conflict during + # createCollection. + - name: dropCollection + object: *database0 + arguments: + session: *session0 + collection: &newCollectionName causal-consistency-createCollection-test - name: createCollection object: *database0 arguments: session: *session0 - collection: &newCollectionName newcoll + collection: *newCollectionName expectEvents: - client: *client0 events: - *findEvent + - commandStartedEvent: + commandName: drop + command: + drop: *newCollectionName + lsid: { $$sessionLsid: *session0 } - commandStartedEvent: commandName: create command: From fa869a33ddb716f63dc9195f5f3e201ca98064ce Mon Sep 17 00:00:00 2001 From: Matt Dale <9760375+matthewdale@users.noreply.github.com> Date: Fri, 8 May 2026 12:12:31 -0700 Subject: [PATCH 11/11] Correct test descrption in causal-consistency-clientBulkWrite.yml --- .../tests/causal-consistency-clientBulkWrite.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml index be36281abd..d1902e188e 100644 --- a/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml +++ b/source/causal-consistency/tests/causal-consistency-clientBulkWrite.yml @@ -1,4 +1,4 @@ -description: "causal consistency write commands include afterClusterTime" +description: "causal consistency bulkWrite include afterClusterTime" schemaVersion: "1.3"