diff --git a/src/operations/delete.ts b/src/operations/delete.ts index b709e67a781..94969ad48b5 100644 --- a/src/operations/delete.ts +++ b/src/operations/delete.ts @@ -18,6 +18,8 @@ export interface DeleteOptions extends CommandOperationOptions, WriteConcernOpti collation?: CollationOptions; /** Specify that the update query should only consider plans using the hinted index */ hint?: string | Document; + /** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */ + let?: Document; /** @deprecated use `removeOne` or `removeMany` to implicitly specify the limit */ single?: boolean; @@ -74,6 +76,10 @@ export class DeleteOperation extends CommandOperation { ordered }; + if (options.let) { + command.let = options.let; + } + if (options.explain !== undefined && maxWireVersion(server) < 3) { return callback ? callback(new MongoError(`server ${server.name} does not support explain on delete`)) diff --git a/src/operations/find.ts b/src/operations/find.ts index 20f6277ca75..d667781e874 100644 --- a/src/operations/find.ts +++ b/src/operations/find.ts @@ -61,6 +61,8 @@ export interface FindOptions extends CommandOperationOptions allowPartialResults?: boolean; /** Determines whether to return the record identifier for each document. If true, adds a field $recordId to the returned documents. */ showRecordId?: boolean; + /** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */ + let?: Document; } const SUPPORTS_WRITE_CONCERN_AND_COLLATION = 5; @@ -286,6 +288,10 @@ function makeFindCommand(ns: MongoDBNamespace, filter: Document, options: FindOp findCommand.allowDiskUse = options.allowDiskUse; } + if (options.let) { + findCommand.let = options.let; + } + return findCommand; } diff --git a/src/operations/find_and_modify.ts b/src/operations/find_and_modify.ts index 096b252d1b9..f1a4567697c 100644 --- a/src/operations/find_and_modify.ts +++ b/src/operations/find_and_modify.ts @@ -27,6 +27,8 @@ export interface FindOneAndDeleteOptions extends CommandOperationOptions { projection?: Document; /** Determines which document the operation modifies if the query selects multiple documents. */ sort?: Sort; + /** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */ + let?: Document; } /** @public */ @@ -43,6 +45,8 @@ export interface FindOneAndReplaceOptions extends CommandOperationOptions { sort?: Sort; /** Upsert the document if it does not exist. */ upsert?: boolean; + /** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */ + let?: Document; } /** @public */ @@ -61,6 +65,8 @@ export interface FindOneAndUpdateOptions extends CommandOperationOptions { sort?: Sort; /** Upsert the document if it does not exist. */ upsert?: boolean; + /** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */ + let?: Document; } /** @internal */ @@ -74,6 +80,7 @@ interface FindAndModifyCmdBase { bypassDocumentValidation?: boolean; arrayFilters?: Document[]; maxTimeMS?: number; + let?: Document; writeConcern?: WriteConcern | WriteConcernSettings; } @@ -129,6 +136,10 @@ class FindAndModifyOperation extends CommandOperation { this.cmdBase.writeConcern = options.writeConcern; } + if (options.let) { + this.cmdBase.let = options.let; + } + // force primary read preference this.readPreference = ReadPreference.primary; diff --git a/src/operations/update.ts b/src/operations/update.ts index b3057ebaebe..9c9f21254e3 100644 --- a/src/operations/update.ts +++ b/src/operations/update.ts @@ -25,6 +25,8 @@ export interface UpdateOptions extends CommandOperationOptions { hint?: string | Document; /** When true, creates a new document if no document matches the query */ upsert?: boolean; + /** Map of parameter names and values that can be accessed using $$var (requires MongoDB 5.0). */ + let?: Document; } /** @public */ @@ -97,6 +99,10 @@ export class UpdateOperation extends CommandOperation { command.bypassDocumentValidation = options.bypassDocumentValidation; } + if (options.let) { + command.let = options.let; + } + const statementWithCollation = this.statements.find(statement => !!statement.collation); if ( collationNotSupported(server, options) || diff --git a/test/functional/unified-spec-runner/operations.ts b/test/functional/unified-spec-runner/operations.ts index 0f94b709aef..5b4ed5c484a 100644 --- a/test/functional/unified-spec-runner/operations.ts +++ b/test/functional/unified-spec-runner/operations.ts @@ -215,7 +215,8 @@ operations.set('createIndex', async ({ entities, operation }) => { operations.set('deleteOne', async ({ entities, operation }) => { const collection = entities.getEntity('collection', operation.object); - return collection.deleteOne(operation.arguments.filter); + const { filter, ...options } = operation.arguments; + return collection.deleteOne(filter, options); }); operations.set('dropCollection', async ({ entities, operation }) => { @@ -230,8 +231,8 @@ operations.set('endSession', async ({ entities, operation }) => { operations.set('find', async ({ entities, operation }) => { const collection = entities.getEntity('collection', operation.object); - const { filter, sort, batchSize, limit } = operation.arguments; - return collection.find(filter, { sort, batchSize, limit }).toArray(); + const { filter, sort, batchSize, limit, let: vars } = operation.arguments; + return collection.find(filter, { sort, batchSize, limit, let: vars }).toArray(); }); operations.set('findOneAndReplace', async ({ entities, operation }) => { @@ -398,12 +399,14 @@ operations.set('runCommand', async ({ entities, operation }: OperationFunctionPa operations.set('updateMany', async ({ entities, operation }) => { const collection = entities.getEntity('collection', operation.object); - return collection.updateMany(operation.arguments.filter, operation.arguments.update); + const { filter, update, ...options } = operation.arguments; + return collection.updateMany(filter, update, options); }); operations.set('updateOne', async ({ entities, operation }) => { const collection = entities.getEntity('collection', operation.object); - return collection.updateOne(operation.arguments.filter, operation.arguments.update); + const { filter, update, ...options } = operation.arguments; + return collection.updateOne(filter, update, options); }); export async function executeOperationAndCheck( diff --git a/test/spec/crud/unified/aggregate-let.json b/test/spec/crud/unified/aggregate-let.json index f31c6ee9df8..b031b64cf07 100644 --- a/test/spec/crud/unified/aggregate-let.json +++ b/test/spec/crud/unified/aggregate-let.json @@ -44,6 +44,109 @@ "minServerVersion": "5.0" } ], + "operations": [ + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "pipeline": [ + { + "$match": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + } + }, + { + "$project": { + "_id": 0, + "x": "$$x", + "y": "$$y", + "rand": "$$rand" + } + } + ], + "let": { + "id": 1, + "x": "foo", + "y": { + "$literal": "bar" + }, + "rand": { + "$rand": {} + } + } + }, + "expectResult": [ + { + "x": "foo", + "y": "bar", + "rand": { + "$$type": "double" + } + } + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "aggregate": "coll0", + "pipeline": [ + { + "$match": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + } + }, + { + "$project": { + "_id": 0, + "x": "$$x", + "y": "$$y", + "rand": "$$rand" + } + } + ], + "let": { + "id": 1, + "x": "foo", + "y": { + "$literal": "bar" + }, + "rand": { + "$rand": {} + } + } + } + } + } + ] + } + ] + }, + { + "description": "Aggregate with let option and dollar-prefixed $literal value", + "runOnRequirements": [ + { + "minServerVersion": "5.0", + "topologies": [ + "single", + "replicaset" + ] + } + ], "operations": [ { "name": "aggregate", diff --git a/test/spec/crud/unified/aggregate-let.yml b/test/spec/crud/unified/aggregate-let.yml index 9ec671ad3cf..ef6ce1558e7 100644 --- a/test/spec/crud/unified/aggregate-let.yml +++ b/test/spec/crud/unified/aggregate-let.yml @@ -22,9 +22,45 @@ initialData: &initialData - { _id: 1 } tests: + # TODO: Once SERVER-57403 is resolved, this test can be removed in favor of + # the "dollar-prefixed $literal value" test below. - description: "Aggregate with let option" runOnRequirements: - minServerVersion: "5.0" + operations: + - name: aggregate + object: *collection0 + arguments: + pipeline: &pipeline0 + # $match takes a query expression, so $expr is necessary to utilize + # an aggregate expression context and access "let" variables. + - $match: { $expr: { $eq: ["$_id", "$$id"] } } + - $project: { _id: 0, x: "$$x", y: "$$y", rand: "$$rand" } + # Values in "let" must be constant or closed expressions that do not + # depend on document values. This test demonstrates a basic constant + # value, a value wrapped with $literal (to avoid expression parsing), + # and a closed expression (e.g. $rand). + let: &let0 + id: 1 + x: foo + y: { $literal: "bar" } + rand: { $rand: {} } + expectResult: + - { x: "foo", y: "bar", rand: { $$type: "double" } } + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + aggregate: *collection0Name + pipeline: *pipeline0 + let: *let0 + + - description: "Aggregate with let option and dollar-prefixed $literal value" + runOnRequirements: + - minServerVersion: "5.0" + # TODO: Remove topology restrictions once SERVER-57403 is resolved + topologies: ["single", "replicaset"] operations: - name: aggregate object: *collection0 diff --git a/test/spec/crud/unified/crud-let.json b/test/spec/crud/unified/crud-let.json new file mode 100644 index 00000000000..d0612dfd580 --- /dev/null +++ b/test/spec/crud/unified/crud-let.json @@ -0,0 +1,513 @@ +{ + "description": "crud-let", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": "foo" + } + ] + } + ], + "tests": [ + { + "description": "Find with let option", + "runOnRequirements": [ + { + "minServerVersion": "5.0" + } + ], + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "let": { + "id": 1 + } + }, + "expectResult": [ + { + "x": "foo" + } + ] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "coll0", + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "let": { + "id": 1 + } + } + } + } + ] + } + ] + }, + { + "description": "Find with let option unsupported (server-side error)", + "runOnRequirements": [ + { + "minServerVersion": "4.2.0", + "maxServerVersion": "4.4.99" + } + ], + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": { + "$match": { + "_id": 1 + } + }, + "let": { + "x": "foo" + } + }, + "expectError": { + "errorContains": "Unrecognized field 'let'", + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "coll0", + "filter": { + "$match": { + "_id": 1 + } + }, + "let": { + "x": "foo" + } + } + } + } + ] + } + ] + }, + { + "description": "FindOneAndUpdate with let option", + "runOnRequirements": [ + { + "minServerVersion": "5.0" + } + ], + "operations": [ + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "update": { + "$set": {} + }, + "let": { + "id": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "coll0", + "query": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "update": { + "$set": {} + }, + "let": { + "id": 1 + } + } + } + } + ] + } + ] + }, + { + "description": "FindOneAndUpdate with let option unsupported (server-side error)", + "runOnRequirements": [ + { + "minServerVersion": "4.2.0", + "maxServerVersion": "4.4.99" + } + ], + "operations": [ + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "filter": { + "$match": { + "_id": 1 + } + }, + "update": { + "$set": {} + }, + "let": { + "x": "foo" + } + }, + "expectError": { + "errorContains": "'let' is an unknown field", + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "findAndModify": "coll0", + "query": { + "$match": { + "_id": 1 + } + }, + "update": { + "$set": {} + }, + "let": { + "x": "foo" + } + } + } + } + ] + } + ] + }, + { + "description": "Update with let option", + "runOnRequirements": [ + { + "minServerVersion": "5.0" + } + ], + "operations": [ + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "update": { + "$set": {} + }, + "let": { + "id": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "u": { + "$set": {} + } + } + ], + "let": { + "id": 1 + } + } + } + } + ] + } + ] + }, + { + "description": "Update with let option unsupported (server-side error)", + "runOnRequirements": [ + { + "minServerVersion": "4.2.0", + "maxServerVersion": "4.4.99" + } + ], + "operations": [ + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "update": { + "$set": {} + }, + "let": { + "id": 1 + } + }, + "expectError": { + "errorContains": "'update.let' is an unknown field", + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "coll0", + "updates": [ + { + "q": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "u": { + "$set": {} + } + } + ], + "let": { + "id": 1 + } + } + } + } + ] + } + ] + }, + { + "description": "Delete with let option", + "runOnRequirements": [ + { + "minServerVersion": "5.0" + } + ], + "operations": [ + { + "name": "deleteOne", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "let": { + "id": 10 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "delete": "coll0", + "deletes": [ + { + "q": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "limit": 1 + } + ], + "let": { + "id": 10 + } + } + } + } + ] + } + ] + }, + { + "description": "Delete with let option unsupported (server-side error)", + "runOnRequirements": [ + { + "minServerVersion": "4.2.0", + "maxServerVersion": "4.4.99" + } + ], + "operations": [ + { + "name": "deleteOne", + "object": "collection0", + "arguments": { + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "let": { + "id": 10 + } + }, + "expectError": { + "errorContains": "'delete.let' is an unknown field", + "isClientError": false + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "delete": "coll0", + "deletes": [ + { + "q": { + "$expr": { + "$eq": [ + "$_id", + "$$id" + ] + } + }, + "limit": 1 + } + ], + "let": { + "id": 10 + } + } + } + } + ] + } + ] + } + ] +} diff --git a/test/spec/crud/unified/crud-let.yml b/test/spec/crud/unified/crud-let.yml new file mode 100644 index 00000000000..e7bd22b036f --- /dev/null +++ b/test/spec/crud/unified/crud-let.yml @@ -0,0 +1,221 @@ +# NOTE: Not yet committed upstream as a spec test! + +description: "crud-let" + +schemaVersion: "1.0" + +createEntities: + - client: + id: &client0 client0 + observeEvents: [ commandStartedEvent ] + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name crud-tests + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name coll0 + +initialData: &initialData + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: "foo" } + +tests: + - description: "Find with let option" + runOnRequirements: + - minServerVersion: "5.0" + operations: + - name: find + object: *collection0 + arguments: + filter: &query0 + $expr: { $eq: ["$_id", "$$id"] } + let: &let0 + id: 1 + expectResult: + - { x: "foo" } + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + find: *collection0Name + filter: *query0 + let: *let0 + + - description: "Find with let option unsupported (server-side error)" + runOnRequirements: + - minServerVersion: "4.2.0" + maxServerVersion: "4.4.99" + operations: + - name: find + object: *collection0 + arguments: + filter: &query1 + $match: { _id: 1 } + let: &let1 + x: foo + expectError: + errorContains: "Unrecognized field 'let'" + isClientError: false + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + find: *collection0Name + filter: *query1 + let: *let1 + + - description: "FindOneAndUpdate with let option" + runOnRequirements: + - minServerVersion: "5.0" + operations: + - name: findOneAndUpdate + object: *collection0 + arguments: + filter: &query2 + $expr: { $eq: ["$_id", "$$id"] } + update: &update2 + $set: {} + let: &let2 + id: 1 + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + findAndModify: *collection0Name + query: *query2 + update: *update2 + let: *let2 + + - description: "FindOneAndUpdate with let option unsupported (server-side error)" + runOnRequirements: + - minServerVersion: "4.2.0" + maxServerVersion: "4.4.99" + operations: + - name: findOneAndUpdate + object: *collection0 + arguments: + filter: &query3 + $match: { _id: 1 } + update: &update3 + $set: {} + let: &let3 + x: foo + expectError: + errorContains: "'let' is an unknown field" + isClientError: false + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + findAndModify: *collection0Name + query: *query3 + update: *update3 + let: *let3 + + - description: "Update with let option" + runOnRequirements: + - minServerVersion: "5.0" + operations: + - name: updateOne + object: *collection0 + arguments: + filter: &query4 + $expr: { $eq: ["$_id", "$$id"] } + update: &update4 + $set: {} + let: &let4 + id: 1 + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + update: *collection0Name + updates: + - q: *query4 + u: *update4 + let: *let4 + + - description: "Update with let option unsupported (server-side error)" + runOnRequirements: + - minServerVersion: "4.2.0" + maxServerVersion: "4.4.99" + operations: + - name: updateOne + object: *collection0 + arguments: + filter: &query5 + $expr: { $eq: ["$_id", "$$id"] } + update: &update5 + $set: {} + let: &let5 + id: 1 + expectError: + errorContains: "'update.let' is an unknown field" + isClientError: false + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + update: *collection0Name + updates: + - q: *query5 + u: *update5 + let: *let5 + + - description: "Delete with let option" + runOnRequirements: + - minServerVersion: "5.0" + operations: + - name: deleteOne + object: *collection0 + arguments: + filter: &query6 + $expr: { $eq: ["$_id", "$$id"] } + let: &let6 + id: 10 + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + delete: *collection0Name + deletes: + - q: *query6 + limit: 1 + let: *let6 + + - description: "Delete with let option unsupported (server-side error)" + runOnRequirements: + - minServerVersion: "4.2.0" + maxServerVersion: "4.4.99" + operations: + - name: deleteOne + object: *collection0 + arguments: + filter: &query7 + $expr: { $eq: ["$_id", "$$id"] } + let: &let7 + id: 10 + expectError: + errorContains: "'delete.let' is an unknown field" + isClientError: false + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + delete: *collection0Name + deletes: + - q: *query7 + limit: 1 + let: *let7