diff --git a/mongo/collection.go b/mongo/collection.go index dbe238a9e3..4c742946a3 100644 --- a/mongo/collection.go +++ b/mongo/collection.go @@ -598,7 +598,13 @@ func (coll *Collection) updateOrReplace(ctx context.Context, filter bsoncore.Doc } op = op.Let(let) } - + if !multi && uo.Sort != nil { + sort, err := marshal(uo.Sort, coll.bsonOpts, coll.registry) + if err != nil { + return nil, err + } + op = op.Sort(sort) + } if uo.BypassDocumentValidation != nil && *uo.BypassDocumentValidation { op = op.BypassDocumentValidation(*uo.BypassDocumentValidation) } @@ -759,6 +765,7 @@ func (coll *Collection) ReplaceOne(ctx context.Context, filter interface{}, uOpts.Upsert = opt.Upsert uOpts.Hint = opt.Hint uOpts.Let = opt.Let + uOpts.Sort = opt.Sort uOpts.Comment = opt.Comment updateOptions = append(updateOptions, uOpts) } diff --git a/mongo/integration/crud_helpers_test.go b/mongo/integration/crud_helpers_test.go index 3344ac297b..3270dbea33 100644 --- a/mongo/integration/crud_helpers_test.go +++ b/mongo/integration/crud_helpers_test.go @@ -80,6 +80,20 @@ func createHint(mt *mtest.T, val bson.RawValue) interface{} { return hint } +// create a sort document from a bson.RawValue +func createSort(mt *mtest.T, val bson.RawValue) interface{} { + mt.Helper() + + var sort interface{} + switch val.Type { + case bsontype.EmbeddedDocument: + sort = val.Document() + default: + mt.Fatalf("unrecognized sort value type: %s\n", val.Type) + } + return sort +} + // returns true if err is a mongo.CommandError containing a code that is expected from a killAllSessions command. func isExpectedKillAllSessionsError(err error) bool { cmdErr, ok := err.(mongo.CommandError) @@ -890,6 +904,8 @@ func executeUpdateOne(mt *mtest.T, sess mongo.Session, args bson.Raw) (*mongo.Up opts = opts.SetCollation(createCollation(mt, val.Document())) case "hint": opts = opts.SetHint(createHint(mt, val)) + case "sort": + opts = opts.SetSort(createSort(mt, val)) case "session": default: mt.Fatalf("unrecognized updateOne option: %v", key) @@ -938,6 +954,8 @@ func executeUpdateMany(mt *mtest.T, sess mongo.Session, args bson.Raw) (*mongo.U opts = opts.SetCollation(createCollation(mt, val.Document())) case "hint": opts = opts.SetHint(createHint(mt, val)) + case "sort": + opts = opts.SetSort(createSort(mt, val)) case "session": default: mt.Fatalf("unrecognized updateMany option: %v", key) @@ -982,6 +1000,8 @@ func executeReplaceOne(mt *mtest.T, sess mongo.Session, args bson.Raw) (*mongo.U opts = opts.SetCollation(createCollation(mt, val.Document())) case "hint": opts = opts.SetHint(createHint(mt, val)) + case "sort": + opts = opts.SetSort(createSort(mt, val)) case "session": default: mt.Fatalf("unrecognized replaceOne option: %v", key) diff --git a/mongo/integration/unified/crud_helpers.go b/mongo/integration/unified/crud_helpers.go index 0a01685988..1115bda177 100644 --- a/mongo/integration/unified/crud_helpers.go +++ b/mongo/integration/unified/crud_helpers.go @@ -63,6 +63,8 @@ func createUpdateArguments(args bson.Raw) (*updateArguments, error) { ua.opts.SetHint(hint) case "let": ua.opts.SetLet(val.Document()) + case "sort": + ua.opts.SetSort(val.Document()) case "update": ua.update, err = createUpdateValue(val) if err != nil { diff --git a/mongo/options/replaceoptions.go b/mongo/options/replaceoptions.go index f7d3960194..ecd03dd98f 100644 --- a/mongo/options/replaceoptions.go +++ b/mongo/options/replaceoptions.go @@ -40,6 +40,12 @@ type ReplaceOptions struct { // Values must be constant or closed expressions that do not reference document fields. Parameters can then be // accessed as variables in an aggregate expression context (e.g. "$$var"). Let interface{} + + // A document specifying which document should be replaced if the filter used by the operation matches multiple + // documents in the collection. If set, the first document in the sorted order will be updated. This option is + // only valid for MongoDB versions >= 8.0. The driver will return an error if the sort parameter is a multi-key + // map. The default value is nil. + Sort interface{} } // Replace creates a new ReplaceOptions instance. @@ -83,6 +89,12 @@ func (ro *ReplaceOptions) SetLet(l interface{}) *ReplaceOptions { return ro } +// SetSort sets the value for the Sort field. +func (ro *ReplaceOptions) SetSort(s interface{}) *ReplaceOptions { + ro.Sort = s + return ro +} + // MergeReplaceOptions combines the given ReplaceOptions instances into a single ReplaceOptions in a last-one-wins // fashion. // @@ -112,6 +124,9 @@ func MergeReplaceOptions(opts ...*ReplaceOptions) *ReplaceOptions { if ro.Let != nil { rOpts.Let = ro.Let } + if ro.Sort != nil { + rOpts.Sort = ro.Sort + } } return rOpts diff --git a/mongo/options/updateoptions.go b/mongo/options/updateoptions.go index 5206f9f01b..4032842c8e 100644 --- a/mongo/options/updateoptions.go +++ b/mongo/options/updateoptions.go @@ -45,6 +45,12 @@ type UpdateOptions struct { // Values must be constant or closed expressions that do not reference document fields. Parameters can then be // accessed as variables in an aggregate expression context (e.g. "$$var"). Let interface{} + + // A document specifying which document should be updated if the filter used by the operation matches multiple + // documents in the collection. If set, the first document in the sorted order will be updated. This option is + // only valid for MongoDB versions >= 8.0. The driver will return an error if the sort parameter is a multi-key + // map. The default value is nil. + Sort interface{} } // Update creates a new UpdateOptions instance. @@ -94,6 +100,12 @@ func (uo *UpdateOptions) SetLet(l interface{}) *UpdateOptions { return uo } +// SetSort sets the value for the Sort field. +func (uo *UpdateOptions) SetSort(s interface{}) *UpdateOptions { + uo.Sort = s + return uo +} + // MergeUpdateOptions combines the given UpdateOptions instances into a single UpdateOptions in a last-one-wins fashion. // // Deprecated: Merging options structs will not be supported in Go Driver 2.0. Users should create a @@ -125,6 +137,9 @@ func MergeUpdateOptions(opts ...*UpdateOptions) *UpdateOptions { if uo.Let != nil { uOpts.Let = uo.Let } + if uo.Sort != nil { + uOpts.Sort = uo.Sort + } } return uOpts diff --git a/testdata/command-monitoring/updateMany-sort.json b/testdata/command-monitoring/updateMany-sort.json new file mode 100644 index 0000000000..25adf1574a --- /dev/null +++ b/testdata/command-monitoring/updateMany-sort.json @@ -0,0 +1,125 @@ +{ + "description": "updateMany", + "schemaVersion": "1.0", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent", + "commandFailedEvent" + ] + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "command-monitoring-tests" + } + }, + { + "collection": { + "id": "collection", + "database": "database", + "collectionName": "test" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "command-monitoring-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "A successful updateMany with sort", + "operations": [ + { + "name": "updateMany", + "object": "collection", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "upsert": { + "$$unsetOrMatches": false + }, + "multi": true + } + ], + "ordered": true + }, + "commandName": "update", + "databaseName": "command-monitoring-tests" + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "n": 2 + }, + "commandName": "update" + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/testdata/command-monitoring/updateMany-sort.yml b/testdata/command-monitoring/updateMany-sort.yml new file mode 100644 index 0000000000..ccbe9a97c7 --- /dev/null +++ b/testdata/command-monitoring/updateMany-sort.yml @@ -0,0 +1,57 @@ +description: "updateMany" + +schemaVersion: "1.0" + +runOnRequirements: + - minServerVersion: "8.0" + +createEntities: + - client: + id: &client client + observeEvents: + - commandStartedEvent + - commandSucceededEvent + - commandFailedEvent + - database: + id: &database database + client: *client + databaseName: &databaseName command-monitoring-tests + - collection: + id: &collection collection + database: *database + collectionName: &collectionName test + +initialData: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + +tests: + - description: "A successful updateMany with sort" + operations: + - name: updateMany + object: *collection + arguments: + filter: { _id: { $gt: 1 } } + sort: { _id: -1 } + update: { $inc: { x: 1 } } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + update: *collectionName + updates: + - q: { _id: { $gt: 1 } } + u: { $inc: { x: 1 } } + upsert: { $$unsetOrMatches: false } + multi: true + ordered: true + commandName: update + databaseName: *databaseName + - commandSucceededEvent: + reply: { ok: 1, n: 2 } + commandName: update diff --git a/testdata/command-monitoring/updateOne-sort.json b/testdata/command-monitoring/updateOne-sort.json new file mode 100644 index 0000000000..3fc9f7bcc7 --- /dev/null +++ b/testdata/command-monitoring/updateOne-sort.json @@ -0,0 +1,130 @@ +{ + "description": "updateOne-sort", + "schemaVersion": "1.0", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent", + "commandFailedEvent" + ] + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "command-monitoring-tests" + } + }, + { + "collection": { + "id": "collection", + "database": "database", + "collectionName": "test" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "command-monitoring-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "tests": [ + { + "description": "A successful updateOne with sort", + "operations": [ + { + "name": "updateOne", + "object": "collection", + "arguments": { + "filter": { + "_id": { + "$gt": 1 + } + }, + "sort": { + "_id": -1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "update": "test", + "updates": [ + { + "q": { + "_id": { + "$gt": 1 + } + }, + "u": { + "$inc": { + "x": 1 + } + }, + "sort": { + "_id": -1 + }, + "upsert": { + "$$unsetOrMatches": false + }, + "multi": { + "$$unsetOrMatches": false + } + } + ], + "ordered": true + }, + "commandName": "update", + "databaseName": "command-monitoring-tests" + } + }, + { + "commandSucceededEvent": { + "reply": { + "ok": 1, + "n": 1 + }, + "commandName": "update" + } + } + ] + } + ] + } + ] +} \ No newline at end of file diff --git a/testdata/command-monitoring/updateOne-sort.yml b/testdata/command-monitoring/updateOne-sort.yml new file mode 100644 index 0000000000..4653bde72b --- /dev/null +++ b/testdata/command-monitoring/updateOne-sort.yml @@ -0,0 +1,58 @@ +description: "updateOne-sort" + +schemaVersion: "1.0" + +runOnRequirements: + - minServerVersion: "8.0" + +createEntities: + - client: + id: &client client + observeEvents: + - commandStartedEvent + - commandSucceededEvent + - commandFailedEvent + - database: + id: &database database + client: *client + databaseName: &databaseName command-monitoring-tests + - collection: + id: &collection collection + database: *database + collectionName: &collectionName test + +initialData: + - collectionName: *collectionName + databaseName: *databaseName + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + +tests: + - description: "A successful updateOne with sort" + operations: + - name: updateOne + object: *collection + arguments: + filter: { _id: { $gt: 1 } } + sort: { _id: -1 } + update: { $inc: { x: 1 } } + expectEvents: + - client: *client + events: + - commandStartedEvent: + command: + update: *collectionName + updates: + - q: { _id: { $gt: 1 } } + u: { $inc: { x: 1 } } + sort: { _id: -1 } + upsert: { $$unsetOrMatches: false } + multi: { $$unsetOrMatches: false } + ordered: true + commandName: update + databaseName: *databaseName + - commandSucceededEvent: + reply: { ok: 1, n: 1 } + commandName: update diff --git a/x/mongo/driver/operation/update.go b/x/mongo/driver/operation/update.go index 1070e7ca70..95a81410d9 100644 --- a/x/mongo/driver/operation/update.go +++ b/x/mongo/driver/operation/update.go @@ -46,6 +46,7 @@ type Update struct { crypt driver.Crypt serverAPI *driver.ServerAPIOptions let bsoncore.Document + sort bsoncore.Document timeout *time.Duration logger *logger.Logger } @@ -204,6 +205,9 @@ func (u *Update) command(dst []byte, desc description.SelectedServer) ([]byte, e if u.let != nil { dst = bsoncore.AppendDocumentElement(dst, "let", u.let) } + if u.sort != nil { + dst = bsoncore.AppendDocumentElement(dst, "sort", u.sort) + } return dst, nil } @@ -397,6 +401,17 @@ func (u *Update) Let(let bsoncore.Document) *Update { return u } +// Sort determines which document the operation updates if the query matches multiple documents. +// The first document matched by the sort order will be updated. +func (u *Update) Sort(sort bsoncore.Document) *Update { + if u == nil { + u = new(Update) + } + + u.sort = sort + return u +} + // Timeout sets the timeout for this operation. func (u *Update) Timeout(timeout *time.Duration) *Update { if u == nil {