From 3fbe90832bb61de425c0dbf7456adddeac07308f Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Tue, 15 Jul 2025 14:10:33 +0200 Subject: [PATCH 1/4] RUBY-3520 Sort for updateOne and replaceOne --- lib/mongo/bulk_write/transformable.rb | 2 + lib/mongo/collection.rb | 10 ++ lib/mongo/collection/view/writable.rb | 12 +++ spec/runners/unified/crud_operations.rb | 9 +- .../bulkWrite-replaceOne-sort.yml | 94 ++++++++++++++++++ .../crud_unified/bulkWrite-updateOne-sort.yml | 94 ++++++++++++++++++ .../data/crud_unified/replaceOne-sort.yml | 94 ++++++++++++++++++ .../data/crud_unified/updateOne-sort.yml | 96 +++++++++++++++++++ 8 files changed, 409 insertions(+), 2 deletions(-) create mode 100644 spec/spec_tests/data/crud_unified/bulkWrite-replaceOne-sort.yml create mode 100644 spec/spec_tests/data/crud_unified/bulkWrite-updateOne-sort.yml create mode 100644 spec/spec_tests/data/crud_unified/replaceOne-sort.yml create mode 100644 spec/spec_tests/data/crud_unified/updateOne-sort.yml diff --git a/lib/mongo/bulk_write/transformable.rb b/lib/mongo/bulk_write/transformable.rb index 9643afb63a..d682d0ba98 100644 --- a/lib/mongo/bulk_write/transformable.rb +++ b/lib/mongo/bulk_write/transformable.rb @@ -99,6 +99,7 @@ module Transformable d['upsert'] = true if doc[:upsert] d[Operation::COLLATION] = doc[:collation] if doc[:collation] d['hint'] = doc[:hint] if doc[:hint] + d['sort'] = doc[:sort] if doc[:sort] end } @@ -130,6 +131,7 @@ module Transformable d[Operation::COLLATION] = doc[:collation] if doc[:collation] d[Operation::ARRAY_FILTERS] = doc[:array_filters] if doc[:array_filters] d['hint'] = doc[:hint] if doc[:hint] + d['sort'] = doc[:sort] if doc[:sort] end } diff --git a/lib/mongo/collection.rb b/lib/mongo/collection.rb index b9cbefee0c..6cabafa5e2 100644 --- a/lib/mongo/collection.rb +++ b/lib/mongo/collection.rb @@ -1049,6 +1049,11 @@ def parallel_scan(cursor_count, options = {}) # May be specified as a Hash (e.g. { _id: 1 }) or a String (e.g. "_id_"). # @option options [ Hash ] :let Mapping of variables to use in the command. # See the server documentation for details. + # @option opts [ Hash ] :sort Specifies which document the operation + # replaces if the query matches multiple documents. The first document + # matched by the sort order will be replaced. + # This option is only supported by servers >= 8.0. Older servers will + # report an error for using this option. # # @return [ Result ] The response from the database. # @@ -1115,6 +1120,11 @@ def update_many(filter, update, options = {}) # May be specified as a Hash (e.g. { _id: 1 }) or a String (e.g. "_id_"). # @option options [ Hash ] :let Mapping of variables to use in the command. # See the server documentation for details. + # @option opts [ Hash ] :sort Specifies which document the operation + # updates if the query matches multiple documents. The first document + # matched by the sort order will be updated. + # This option is only supported by servers >= 8.0. Older servers will + # report an error for using this option. # # @return [ Result ] The response from the database. # diff --git a/lib/mongo/collection/view/writable.rb b/lib/mongo/collection/view/writable.rb index 0a1f553b1d..c13155d911 100644 --- a/lib/mongo/collection/view/writable.rb +++ b/lib/mongo/collection/view/writable.rb @@ -389,6 +389,11 @@ def delete_one(opts = {}) # @option opts [ true, false ] :upsert Whether to upsert if the # document doesn't exist. # Can be :w => Integer, :fsync => Boolean, :j => Boolean. + # @option opts [ Hash ] :sort Specifies which document the operation + # replaces if the query matches multiple documents. The first document + # matched by the sort order will be replaced. + # This option is only supported by servers >= 8.0. Older servers will + # report an error for using this option. # # @return [ Result ] The response from the database. # @@ -410,6 +415,7 @@ def replace_one(replacement, opts = {}) Operation::U => replacement, hint: opts[:hint], collation: opts[:collation] || opts['collation'] || collation, + sort: opts[:sort] || opts['sort'], }.compact if opts[:upsert] update_doc['upsert'] = true @@ -549,6 +555,11 @@ def update_many(spec, opts = {}) # document doesn't exist. # @option opts [ Hash ] :write_concern The write concern options. # Can be :w => Integer, :fsync => Boolean, :j => Boolean. + # @option opts [ Hash ] :sort Specifies which document the operation + # updates if the query matches multiple documents. The first document + # matched by the sort order will be updated. + # This option is only supported by servers >= 8.0. Older servers will + # report an error for using this option. # # @return [ Result ] The response from the database. # @@ -570,6 +581,7 @@ def update_one(spec, opts = {}) Operation::U => spec, hint: opts[:hint], collation: opts[:collation] || opts['collation'] || collation, + sort: opts[:sort] || opts['sort'], }.compact if opts[:upsert] update_doc['upsert'] = true diff --git a/spec/runners/unified/crud_operations.rb b/spec/runners/unified/crud_operations.rb index fb2387ce6e..5a418973fc 100644 --- a/spec/runners/unified/crud_operations.rb +++ b/spec/runners/unified/crud_operations.rb @@ -194,7 +194,8 @@ def update_one(op) hint: args.use('hint'), upsert: args.use('upsert'), timeout_ms: args.use('timeoutMS'), - max_time_ms: args.use('maxTimeMS') + max_time_ms: args.use('maxTimeMS'), + sort: args.use('sort') } if session = args.use('session') opts[:session] = entities.get(:session, session) @@ -228,7 +229,8 @@ def replace_one(op) let: args.use('let'), hint: args.use('hint'), timeout_ms: args.use('timeoutMS'), - max_time_ms: args.use('maxTimeMS') + max_time_ms: args.use('maxTimeMS'), + sort: args.use('sort'), ) end end @@ -358,6 +360,9 @@ def convert_bulk_write_spec(spec) else raise NotImplementedError, "Unknown operation #{op}" end + if %w[ updateOne replaceOne ].include?(op) + out[:sort] = spec.use('sort') if spec.key?('sort') + end unless spec.empty? raise NotImplementedError, "Unhandled keys: #{spec}" end diff --git a/spec/spec_tests/data/crud_unified/bulkWrite-replaceOne-sort.yml b/spec/spec_tests/data/crud_unified/bulkWrite-replaceOne-sort.yml new file mode 100644 index 0000000000..6f326fe043 --- /dev/null +++ b/spec/spec_tests/data/crud_unified/bulkWrite-replaceOne-sort.yml @@ -0,0 +1,94 @@ +description: BulkWrite replaceOne-sort + +schemaVersion: "1.0" + +createEntities: + - client: + id: &client0 client0 + observeEvents: [ commandStartedEvent, commandSucceededEvent ] + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name crud-tests + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name coll0 + +initialData: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + +tests: + - description: BulkWrite replaceOne with sort option + runOnRequirements: + - minServerVersion: "8.0" + operations: + - object: *collection0 + name: bulkWrite + arguments: + requests: + - replaceOne: + filter: { _id: { $gt: 1 } } + sort: { _id: -1 } + replacement: { x: 1 } + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + update: *collection0Name + updates: + - q: { _id: { $gt: 1 } } + u: { x: 1 } + sort: { _id: -1 } + multi: { $$unsetOrMatches: false } + upsert: { $$unsetOrMatches: false } + - commandSucceededEvent: + reply: { ok: 1, n: 1 } + commandName: update + outcome: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 1 } + + - description: BulkWrite replaceOne with sort option unsupported (server-side error) + runOnRequirements: + - maxServerVersion: "7.99" + operations: + - object: *collection0 + name: bulkWrite + arguments: + requests: + - replaceOne: + filter: { _id: { $gt: 1 } } + sort: { _id: -1 } + replacement: { x: 1 } + expectError: + isClientError: false + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + update: *collection0Name + updates: + - q: { _id: { $gt: 1 } } + u: { x: 1 } + sort: { _id: -1 } + multi: { $$unsetOrMatches: false } + upsert: { $$unsetOrMatches: false } + outcome: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } diff --git a/spec/spec_tests/data/crud_unified/bulkWrite-updateOne-sort.yml b/spec/spec_tests/data/crud_unified/bulkWrite-updateOne-sort.yml new file mode 100644 index 0000000000..72bc814d69 --- /dev/null +++ b/spec/spec_tests/data/crud_unified/bulkWrite-updateOne-sort.yml @@ -0,0 +1,94 @@ +description: BulkWrite updateOne-sort + +schemaVersion: "1.0" + +createEntities: + - client: + id: &client0 client0 + observeEvents: [ commandStartedEvent, commandSucceededEvent ] + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name crud-tests + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name coll0 + +initialData: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + +tests: + - description: BulkWrite updateOne with sort option + runOnRequirements: + - minServerVersion: "8.0" + operations: + - object: *collection0 + name: bulkWrite + arguments: + requests: + - updateOne: + filter: { _id: { $gt: 1 } } + sort: { _id: -1 } + update: [ $set: { x: 1 } ] + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + update: *collection0Name + updates: + - q: { _id: { $gt: 1 } } + u: [ $set: { x: 1 } ] + sort: { _id: -1 } + multi: { $$unsetOrMatches: false } + upsert: { $$unsetOrMatches: false } + - commandSucceededEvent: + reply: { ok: 1, n: 1 } + commandName: update + outcome: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 1 } + + - description: BulkWrite updateOne with sort option unsupported (server-side error) + runOnRequirements: + - maxServerVersion: "7.99" + operations: + - object: *collection0 + name: bulkWrite + arguments: + requests: + - updateOne: + filter: { _id: { $gt: 1 } } + sort: { _id: -1 } + update: [ $set: { x: 1 } ] + expectError: + isClientError: false + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + update: *collection0Name + updates: + - q: { _id: { $gt: 1 } } + u: [ $set: { x: 1 } ] + sort: { _id: -1 } + multi: { $$unsetOrMatches: false } + upsert: { $$unsetOrMatches: false } + outcome: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } diff --git a/spec/spec_tests/data/crud_unified/replaceOne-sort.yml b/spec/spec_tests/data/crud_unified/replaceOne-sort.yml new file mode 100644 index 0000000000..f4b10fbaf9 --- /dev/null +++ b/spec/spec_tests/data/crud_unified/replaceOne-sort.yml @@ -0,0 +1,94 @@ +description: replaceOne-sort + +schemaVersion: "1.0" + +createEntities: + - client: + id: &client0 client0 + observeEvents: [ commandStartedEvent, commandSucceededEvent ] + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name crud-tests + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name coll0 + +initialData: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + +tests: + - description: ReplaceOne with sort option + runOnRequirements: + - minServerVersion: "8.0" + operations: + - name: replaceOne + object: *collection0 + arguments: + filter: { _id: { $gt: 1 } } + sort: { _id: -1 } + replacement: { x: 1 } + expectResult: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + update: *collection0Name + updates: + - q: { _id: { $gt: 1 } } + u: { x: 1 } + sort: { _id: -1 } + multi: { $$unsetOrMatches: false } + upsert: { $$unsetOrMatches: false } + - commandSucceededEvent: + reply: { ok: 1, n: 1 } + commandName: update + outcome: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 1 } + + - description: replaceOne with sort option unsupported (server-side error) + runOnRequirements: + - maxServerVersion: "7.99" + operations: + - name: replaceOne + object: *collection0 + arguments: + filter: { _id: { $gt: 1 } } + sort: { _id: -1 } + replacement: { x: 1 } + expectError: + isClientError: false + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + update: *collection0Name + updates: + - q: { _id: { $gt: 1 } } + u: { x: 1 } + sort: { _id: -1 } + multi: { $$unsetOrMatches: false } + upsert: { $$unsetOrMatches: false } + outcome: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } diff --git a/spec/spec_tests/data/crud_unified/updateOne-sort.yml b/spec/spec_tests/data/crud_unified/updateOne-sort.yml new file mode 100644 index 0000000000..a14e1df1d2 --- /dev/null +++ b/spec/spec_tests/data/crud_unified/updateOne-sort.yml @@ -0,0 +1,96 @@ +description: updateOne-sort + +schemaVersion: "1.0" + +createEntities: + - client: + id: &client0 client0 + observeEvents: + - commandStartedEvent + - commandSucceededEvent + - database: + id: &database0 database0 + client: *client0 + databaseName: &database0Name crud-tests + - collection: + id: &collection0 collection0 + database: *database0 + collectionName: &collection0Name coll0 + +initialData: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } + +tests: + - description: UpdateOne with sort option + runOnRequirements: + - minServerVersion: "8.0" + operations: + - name: updateOne + object: *collection0 + arguments: + filter: { _id: { $gt: 1 } } + sort: { _id: -1 } + update: { $inc: { x: 1 } } + expectResult: + matchedCount: 1 + modifiedCount: 1 + upsertedCount: 0 + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + update: *collection0Name + updates: + - q: { _id: { $gt: 1 } } + u: { $inc: { x: 1 } } + sort: { _id: -1 } + multi: { $$unsetOrMatches: false } + upsert: { $$unsetOrMatches: false } + - commandSucceededEvent: + reply: { ok: 1, n: 1 } + commandName: update + outcome: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 34 } + + - description: updateOne with sort option unsupported (server-side error) + runOnRequirements: + - maxServerVersion: "7.99" + operations: + - name: updateOne + object: *collection0 + arguments: + filter: { _id: { $gt: 1 } } + sort: { _id: -1 } + update: { $inc: { x: 1 } } + expectError: + isClientError: false + expectEvents: + - client: *client0 + events: + - commandStartedEvent: + command: + update: *collection0Name + updates: + - q: { _id: { $gt: 1 } } + u: { $inc: { x: 1 } } + sort: { _id: -1 } + multi: { $$unsetOrMatches: false } + upsert: { $$unsetOrMatches: false } + outcome: + - collectionName: *collection0Name + databaseName: *database0Name + documents: + - { _id: 1, x: 11 } + - { _id: 2, x: 22 } + - { _id: 3, x: 33 } From 18db4b422948d4ed08cfc2af74c65775d1f15fba Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov <160598371+comandeo-mongo@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:53:06 +0200 Subject: [PATCH 2/4] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/mongo/collection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mongo/collection.rb b/lib/mongo/collection.rb index 6cabafa5e2..11ec1d64d9 100644 --- a/lib/mongo/collection.rb +++ b/lib/mongo/collection.rb @@ -1049,7 +1049,7 @@ def parallel_scan(cursor_count, options = {}) # May be specified as a Hash (e.g. { _id: 1 }) or a String (e.g. "_id_"). # @option options [ Hash ] :let Mapping of variables to use in the command. # See the server documentation for details. - # @option opts [ Hash ] :sort Specifies which document the operation + # @option options [ Hash ] :sort Specifies which document the operation # replaces if the query matches multiple documents. The first document # matched by the sort order will be replaced. # This option is only supported by servers >= 8.0. Older servers will From b976f00ae06386cdd8f1d8b0642076afaae9a76e Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov <160598371+comandeo-mongo@users.noreply.github.com> Date: Mon, 1 Sep 2025 17:53:14 +0200 Subject: [PATCH 3/4] Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- lib/mongo/collection.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/mongo/collection.rb b/lib/mongo/collection.rb index 11ec1d64d9..b0f2999123 100644 --- a/lib/mongo/collection.rb +++ b/lib/mongo/collection.rb @@ -1120,7 +1120,7 @@ def update_many(filter, update, options = {}) # May be specified as a Hash (e.g. { _id: 1 }) or a String (e.g. "_id_"). # @option options [ Hash ] :let Mapping of variables to use in the command. # See the server documentation for details. - # @option opts [ Hash ] :sort Specifies which document the operation + # @option options [ Hash ] :sort Specifies which document the operation # updates if the query matches multiple documents. The first document # matched by the sort order will be updated. # This option is only supported by servers >= 8.0. Older servers will From 1bdccb79965347bd65b5b9ebe0f5245c3e9ba8e5 Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov <160598371+comandeo-mongo@users.noreply.github.com> Date: Thu, 4 Sep 2025 17:58:41 +0200 Subject: [PATCH 4/4] RUBY-3705 Skip tests that fail on latest (#2953) --- spec/mongo/index/view_spec.rb | 2 ++ spec/mongo/operation/drop_index_spec.rb | 1 + 2 files changed, 3 insertions(+) diff --git a/spec/mongo/index/view_spec.rb b/spec/mongo/index/view_spec.rb index 1bab5470f5..5e009e27e3 100644 --- a/spec/mongo/index/view_spec.rb +++ b/spec/mongo/index/view_spec.rb @@ -38,6 +38,8 @@ describe '#drop_one' do + max_server_version '8.2.99' + let(:spec) do { another: -1 } end diff --git a/spec/mongo/operation/drop_index_spec.rb b/spec/mongo/operation/drop_index_spec.rb index 17d976d6cf..fac3da7207 100644 --- a/spec/mongo/operation/drop_index_spec.rb +++ b/spec/mongo/operation/drop_index_spec.rb @@ -5,6 +5,7 @@ describe Mongo::Operation::DropIndex do require_no_required_api_version + max_server_version '8.2.99' before do authorized_collection.indexes.drop_all