diff --git a/docs/reference/crud-operations.txt b/docs/reference/crud-operations.txt index fe7d68c21f..a242dbed38 100644 --- a/docs/reference/crud-operations.txt +++ b/docs/reference/crud-operations.txt @@ -260,6 +260,32 @@ when querying and their corresponding methods as examples. # MongoDB 2.6 client[:artists].find.explain(verbose: true) + The explain operation supports ``:session`` and ``:read`` + (for read preference) options. To specify these options for a single + explain operation, they must be given to the ``find`` method as + follows: + + .. code-block:: ruby + + client[:artists].find({}, session: session).explain + + client[:artists].find({}, read: {mode: :secondary_preferred}).explain + + If the read preference option is specified on the client or on the + collection, it will be passed to the explain operation: + + .. code-block:: ruby + + client[:artists, read: {mode: :secondary_preferred}].find.explain + + Note that the session option is not accepted when creating a collection + object. + + The explain command does not support passing the read concern option. + If the read concern is specifed on the client or collection level, or + if the read concern is specified as a find option, it will NOT be passed + by the driver to the explain command. + .. note:: The information returned by the server for the ``explain`` command diff --git a/lib/mongo/collection.rb b/lib/mongo/collection.rb index aa7a565ffa..e471246260 100644 --- a/lib/mongo/collection.rb +++ b/lib/mongo/collection.rb @@ -237,7 +237,7 @@ def create(opts = {}) # TODO put the list of read options in a class-level constant when # we figure out what the full set of them is. options = Hash[self.options.reject do |key, value| - %w(read read_preference).include?(key.to_s) + %w(read read_preference read_concern).include?(key.to_s) end] operation = { :create => name }.merge(options) operation.delete(:write) @@ -253,12 +253,12 @@ def create(opts = {}) raise Error::UnsupportedCollation end - Operation::Create.new({ - selector: operation, - db_name: database.name, - write_concern: write_concern, - session: session, - }).execute(server, context: Operation::Context.new(client: client, session: session)) + Operation::Create.new( + selector: operation, + db_name: database.name, + write_concern: write_concern, + session: session, + ).execute(server, context: Operation::Context.new(client: client, session: session)) end end @@ -332,8 +332,8 @@ def drop(opts = {}) # @option options [ true, false ] :no_cursor_timeout The server normally times out idle # cursors after an inactivity period (10 minutes) to prevent excess memory use. # Set this option to prevent that. - # @option options [ true, false ] :oplog_replay Internal replication use only - driver - # should not set. + # @option options [ true, false ] :oplog_replay For internal replication + # use only, applications should not set this option. # @option options [ Hash ] :projection The fields to include or exclude from each doc # in the result set. # @option options [ Session ] :session The session to use. diff --git a/lib/mongo/collection/view.rb b/lib/mongo/collection/view.rb index 9fef98bcb3..1d0ee8df64 100644 --- a/lib/mongo/collection/view.rb +++ b/lib/mongo/collection/view.rb @@ -141,6 +141,8 @@ def hash # @option options [ Hash ] :read The read preference to use for the # query. If none is provided, the collection's default read preference # is used. + # @option options [ Hash ] :read_concern The read concern to use for + # the query. # @option options [ true | false ] :show_disk_loc Return disk location # info as a field in each doc. # @option options [ Integer ] :skip The number of documents to skip. @@ -153,7 +155,19 @@ def hash def initialize(collection, filter = {}, options = {}) validate_doc!(filter) @collection = collection - parse_parameters!(BSON::Document.new(filter), BSON::Document.new(options)) + + filter = BSON::Document.new(filter) + options = BSON::Document.new(options) + + # This is when users pass $query in filter and other modifiers + # alongside? + query = filter.delete(:$query) + # This makes modifiers contain the filter if filter wasn't + # given via $query but as top-level keys, presumably + # downstream code ignores non-modifier keys in the modifiers? + modifiers = filter.merge(options.delete(:modifiers) || {}) + @filter = (query || filter).freeze + @options = Operation::Find::Builder::Modifiers.map_driver_options(modifiers).merge!(options).freeze end # Get a human-readable string representation of +View+. @@ -189,13 +203,6 @@ def initialize_copy(other) @filter = other.filter.dup end - def parse_parameters!(filter, options) - query = filter.delete(QUERY) - modifiers = (filter || {}).merge(options.delete(MODIFIERS) || {}) - @filter = (query || filter).freeze - @options = Builder::Modifiers.map_driver_options(modifiers).merge!(options).freeze - end - def new(options) View.new(collection, filter, options) end diff --git a/lib/mongo/collection/view/builder.rb b/lib/mongo/collection/view/builder.rb index 3577f58db2..46cc1dff32 100644 --- a/lib/mongo/collection/view/builder.rb +++ b/lib/mongo/collection/view/builder.rb @@ -17,7 +17,3 @@ require 'mongo/collection/view/builder/aggregation' require 'mongo/collection/view/builder/map_reduce' -require 'mongo/collection/view/builder/op_query' -require 'mongo/collection/view/builder/find_command' -require 'mongo/collection/view/builder/flags' -require 'mongo/collection/view/builder/modifiers' diff --git a/lib/mongo/collection/view/builder/find_command.rb b/lib/mongo/collection/view/builder/find_command.rb deleted file mode 100644 index cde73e7d0c..0000000000 --- a/lib/mongo/collection/view/builder/find_command.rb +++ /dev/null @@ -1,173 +0,0 @@ -# frozen_string_literal: true -# encoding: utf-8 - -# Copyright (C) 2015-2020 MongoDB Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -module Mongo - class Collection - class View - module Builder - - # Builds a find command specification from options. - # - # @since 2.2.0 - class FindCommand - extend Forwardable - - # The mappings from ruby options to the find command. - # - # @since 2.2.0 - MAPPINGS = BSON::Document.new( - sort: 'sort', - projection: 'projection', - hint: 'hint', - skip: 'skip', - limit: 'limit', - batch_size: 'batchSize', - single_batch: 'singleBatch', - comment: 'comment', - max_scan: 'maxScan', - max_time_ms: 'maxTimeMS', - max_value: 'max', - min_value: 'min', - return_key: 'returnKey', - show_disk_loc: 'showRecordId', - snapshot: 'snapshot', - tailable: 'tailable', - tailable_cursor: 'tailable', - oplog_replay: 'oplogReplay', - no_cursor_timeout: 'noCursorTimeout', - await_data: 'awaitData', - allow_partial_results: 'allowPartialResults', - allow_disk_use: 'allowDiskUse', - collation: 'collation' - ).freeze - - # Create the find command builder. - # - # @example Create the find command builder. - # FindCommandBuilder.new(view) - # - # @param [ Collection::View ] view The collection view. - # @param [ Session ] session The session. - # - # @since 2.2.2 - def initialize(view, session) - @view = view - @session = session - end - - def_delegators :@view, :collection, :database, :filter, :options, :read - - # Get the specification to pass to the find command operation. - # - # @example Get the specification. - # builder.specification - # - # @return [ Hash ] The specification. - # - # @since 2.2.0 - def specification - { - selector: find_command, - db_name: database.name, - read: read, - session: @session, - } - end - - # Get the specification for an explain command that wraps the find - # command. - # - # @example Get the explain spec. - # builder.explain_specification - # - # @return [ Hash ] The specification. - # - # @since 2.2.0 - def explain_specification - { - selector: { - explain: find_command, - }, - db_name: database.name, - read: read, - session: @session, - # We should always have options{:explain] set if we are explaining. - # The explain field is not sent to the server, it will be - # processed in the operation layer. - explain: options[:explain], - } - end - - private - - def find_command - document = BSON::Document.new( - find: collection.name, - filter: filter, - ) - if collection.read_concern - document[:readConcern] = Options::Mapper.transform_values_to_strings( - collection.read_concern) - end - command = Options::Mapper.transform_documents( - convert_flags(options), MAPPINGS, document) - if command['oplogReplay'] - log_warn("oplogReplay is deprecated and ignored by MongoDB 4.4 and later") - end - convert_limit_and_batch_size(command) - command - end - - def convert_limit_and_batch_size(command) - if command[:limit] && command[:limit] < 0 && - command[:batchSize] && command[:batchSize] < 0 - - command[:limit] = command[:limit].abs - command[:batchSize] = command[:limit].abs - command[:singleBatch] = true - - else - [:limit, :batchSize].each do |opt| - if command[opt] - if command[opt] < 0 - command[opt] = command[opt].abs - command[:singleBatch] = true - elsif command[opt] == 0 - command.delete(opt) - end - end - end - end - end - - def convert_flags(options) - return options if options.empty? - opts = options.dup - opts.delete(:cursor_type) - Flags.map_flags(options).reduce(opts) do |o, key| - o.merge!(key => true) - end - end - - def log_warn(*args) - database.client.log_warn(*args) - end - end - end - end - end -end diff --git a/lib/mongo/collection/view/builder/map_reduce.rb b/lib/mongo/collection/view/builder/map_reduce.rb index 967847dc53..60c102ab68 100644 --- a/lib/mongo/collection/view/builder/map_reduce.rb +++ b/lib/mongo/collection/view/builder/map_reduce.rb @@ -71,36 +71,6 @@ def initialize(map, reduce, view, options) @options = options end - # Get the specification for issuing a find command on the map/reduce - # results. - # - # @example Get the command specification. - # builder.command_specification - # - # @return [ Hash ] The specification. - # - # @since 2.2.0 - def command_specification - { - selector: find_command, - db_name: query_database, - read: read, - session: options[:session] - } - end - - # Get the specification for the document query after a map/reduce. - # - # @example Get the query specification. - # builder.query_specification - # - # @return [ Hash ] The specification. - # - # @since 2.2.0 - def query_specification - { selector: {}, options: {}, db_name: query_database, coll_name: query_collection } - end - # Get the specification to pass to the map/reduce operation. # # @example Get the specification. @@ -113,6 +83,8 @@ def specification spec = { selector: map_reduce_command, db_name: database.name, + # Note that selector just above may also have a read preference + # specified, per the #map_reduce_command method below. read: read, session: options[:session] } @@ -121,8 +93,6 @@ def specification private - OUT_ACTIONS = [ :replace, :merge, :reduce ].freeze - def write?(spec) if out = spec[:selector][:out] out.is_a?(String) || @@ -130,37 +100,29 @@ def write?(spec) end end - def find_command - BSON::Document.new('find' => query_collection, 'filter' => {}) - end - def map_reduce_command command = BSON::Document.new( :mapReduce => collection.name, :map => map, :reduce => reduce, :query => filter, - :out => { inline: 1 } + :out => { inline: 1 }, ) + # Shouldn't this use self.read ? if collection.read_concern command[:readConcern] = Options::Mapper.transform_values_to_strings( collection.read_concern) end command.merge!(view_options) + # Read preference isn't simply passed in the command payload + # (it may need to be converted to wire protocol flags) + # so remove it here and hopefully it's handled elsewhere. + # If not, RUBY-2706. + command.delete(:read) command.merge!(Options::Mapper.transform_documents(options, MAPPINGS)) command end - def query_database - options[:out].respond_to?(:keys) && options[:out][:db] ? options[:out][:db] : database.name - end - - def query_collection - if options[:out].respond_to?(:keys) - options[:out][OUT_ACTIONS.find { |action| options[:out][action] }] - end || options[:out] - end - def view_options @view_options ||= (opts = view.options.dup opts.delete(:session) diff --git a/lib/mongo/collection/view/builder/op_query.rb b/lib/mongo/collection/view/builder/op_query.rb deleted file mode 100644 index 4debfe99e4..0000000000 --- a/lib/mongo/collection/view/builder/op_query.rb +++ /dev/null @@ -1,94 +0,0 @@ -# frozen_string_literal: true -# encoding: utf-8 - -# Copyright (C) 2015-2020 MongoDB Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -module Mongo - class Collection - class View - module Builder - - # Builds a legacy OP_QUERY specification from options. - # - # @since 2.2.0 - class OpQuery - extend Forwardable - - def_delegators :@view, :cluster, :collection, :database, :filter, :options, :read - - # @return [ BSON::Document ] modifiers The server modifiers. - attr_reader :modifiers - - # Create the new legacy query builder. - # - # @example Create the query builder. - # QueryBuilder.new(view) - # - # @param [ Collection::View ] view The collection view. - # - # @since 2.2.2 - def initialize(view) - @view = view - @modifiers = Modifiers.map_server_modifiers(options) - end - - def specification - { - :selector => requires_special_filter? ? special_filter : filter, - :read => read, - :options => query_options, - :db_name => database.name, - :coll_name => collection.name - } - end - - private - - def query_options - BSON::Document.new( - project: options[:projection], - skip: options[:skip], - limit: options[:limit], - flags: Flags.map_flags(options), - batch_size: options[:batch_size] - ) - end - - def requires_special_filter? - !modifiers.empty? || cluster.sharded? - end - - def read_pref_formatted - @read_formatted ||= begin - if read - read_pref = ServerSelector.get(read).to_mongos - Mongo::Lint.validate_camel_case_read_preference(read_pref) - read_pref - else - nil - end - end - end - - def special_filter - sel = BSON::Document.new(:$query => filter).merge!(modifiers) - sel[:$readPreference] = read_pref_formatted unless read_pref_formatted.nil? - sel - end - end - end - end - end -end diff --git a/lib/mongo/collection/view/iterable.rb b/lib/mongo/collection/view/iterable.rb index b4f766743a..e4573f3456 100644 --- a/lib/mongo/collection/view/iterable.rb +++ b/lib/mongo/collection/view/iterable.rb @@ -130,31 +130,48 @@ def cache_options projection: projection, collation: collation, read_concern: read_concern, - read_preference: read_preference - + read_preference: read_preference, } end def initial_query_op(server, session) - if server.with_connection { |connection| connection.features }.find_command_enabled? - initial_command_op(session) - else - # Server versions that do not have the find command feature - # (versions older than 3.2) do not support the allow_disk_use option - # but perform no validation and will not raise an error if it is - # specified. If the allow_disk_use option is specified, raise an error - # to alert the user. - raise Error::UnsupportedOption.allow_disk_use_error if options.key?(:allow_disk_use) - Operation::Find.new(Builder::OpQuery.new(self).specification) + spec = { + coll_name: collection.name, + filter: filter, + projection: projection, + db_name: database.name, + session: session, + collation: collation, + sort: sort, + skip: skip, + limit: limit, + allow_disk_use: options[:allow_disk_use], + read: read, + read_concern: options[:read_concern] || read_concern, + batch_size: batch_size, + hint: options[:hint], + max_time_ms: options[:max_time_ms], + max_value: options[:max_value], + min_value: options[:min_value], + return_key: options[:return_key], + show_disk_loc: options[:show_disk_loc], + comment: options[:comment], + oplog_replay: if (v = options[:oplog_replay]).nil? + collection.options[:oplog_replay] + else + v + end, + } + + if spec[:oplog_replay] + collection.client.log_warn("The :oplog_replay option is deprecated and ignored by MongoDB 4.4 and later") end - end - def initial_command_op(session) - builder = Builder::FindCommand.new(self, session) if explained? - Operation::Explain.new(builder.explain_specification) + spec[:explain] = options[:explain] + Operation::Explain.new(spec) else - Operation::Find.new(builder.specification) + Operation::Find.new(spec) end end diff --git a/lib/mongo/collection/view/map_reduce.rb b/lib/mongo/collection/view/map_reduce.rb index 6df3d92c96..a81ccd1283 100644 --- a/lib/mongo/collection/view/map_reduce.rb +++ b/lib/mongo/collection/view/map_reduce.rb @@ -156,6 +156,28 @@ def out(location = nil) configure(:out, location) end + # Returns the collection name where the map-reduce result is written to. + # If the result is returned inline, returns nil. + def out_collection_name + if options[:out].respond_to?(:keys) + options[:out][OUT_ACTIONS.find do |action| + options[:out][action] + end] + end || options[:out] + end + + # Returns the database name where the map-reduce result is written to. + # If the result is returned inline, returns nil. + def out_database_name + if options[:out] + if options[:out].respond_to?(:keys) && (db = options[:out][:db]) + db + else + database.name + end + end + end + # Set or get a scope on the operation. # # @example Set the scope value. @@ -207,6 +229,8 @@ def execute private + OUT_ACTIONS = [ :replace, :merge, :reduce ].freeze + def server_selector @view.send(:server_selector) end @@ -257,11 +281,15 @@ def find_command_spec(session) end def fetch_query_op(server, session) - if server.with_connection { |connection| connection.features }.find_command_enabled? - Operation::Find.new(find_command_spec(session)) - else - Operation::Find.new(fetch_query_spec) - end + spec = { + coll_name: out_collection_name, + db_name: out_database_name, + filter: {}, + session: session, + read: read, + read_concern: options[:read_concern] || collection.read_concern, + } + Operation::Find.new(spec) end def send_fetch_query(server, session) diff --git a/lib/mongo/collection/view/readable.rb b/lib/mongo/collection/view/readable.rb index 663ba3e03d..d52f36465e 100644 --- a/lib/mongo/collection/view/readable.rb +++ b/lib/mongo/collection/view/readable.rb @@ -24,16 +24,6 @@ class View # @since 2.0.0 module Readable - # The query modifier constant. - # - # @since 2.2.0 - QUERY = '$query'.freeze - - # The modifiers option constant. - # - # @since 2.2.0 - MODIFIERS = 'modifiers'.freeze - # Execute an aggregation on the collection view. # # @example Aggregate documents. @@ -542,7 +532,11 @@ def sort(spec = nil) configure(:sort, spec) end - # “meta” operators that let you modify the output or behavior of a query. + # If called without arguments or with a nil argument, returns + # the legacy (OP_QUERY) server modifiers for the current view. + # If called with a non-nil argument, which must be a Hash or a + # subclass, merges the provided modifiers into the current view. + # Both string and symbol keys are allowed in the input hash. # # @example Set the modifiers document. # view.modifiers(:$orderby => Mongo::Index::ASCENDING) @@ -553,8 +547,11 @@ def sort(spec = nil) # # @since 2.1.0 def modifiers(doc = nil) - return Builder::Modifiers.map_server_modifiers(options) if doc.nil? - new(options.merge(Builder::Modifiers.map_driver_options(doc))) + if doc.nil? + Operation::Find::Builder::Modifiers.map_server_modifiers(options) + else + new(options.merge(Operation::Find::Builder::Modifiers.map_driver_options(BSON::Document.new(doc)))) + end end # A cumulative time limit in milliseconds for processing get more operations @@ -645,34 +642,36 @@ def server_selector def parallel_scan(cursor_count, options = {}) if options[:session] + # The session would be overwritten by the one in +options+ later. session = client.send(:get_session, @options) else session = nil end server = server_selector.select_server(cluster, nil, session) - cmd = Operation::ParallelScan.new({ - :coll_name => collection.name, - :db_name => database.name, - :cursor_count => cursor_count, - :read_concern => read_concern, - :session => session, - }.merge!(options)) - cmd.execute(server, context: Operation::Context.new(client: client, session: session)).cursor_ids.map do |cursor_id| - result = if server.with_connection { |connection| connection.features }.find_command_enabled? - Operation::GetMore.new({ - :selector => {:getMore => BSON::Int64.new(cursor_id), - :collection => collection.name}, - :db_name => database.name, - :session => session, - }).execute(server, context: Operation::Context.new(client: client, session: session)) - else - Operation::GetMore.new({ - :to_return => 0, - :cursor_id => BSON::Int64.new(cursor_id), - :db_name => database.name, - :coll_name => collection.name - }).execute(server, context: Operation::Context.new(client: client, session: session)) - end + spec = { + coll_name: collection.name, + db_name: database.name, + cursor_count: cursor_count, + read_concern: read_concern, + session: session, + }.update(options) + session = spec[:session] + op = Operation::ParallelScan.new(spec) + # Note that the context object shouldn't be reused for subsequent + # GetMore operations. + context = Operation::Context.new(client: client, session: session) + op.execute(server, context: context).cursor_ids.map do |cursor_id| + spec = { + cursor_id: cursor_id, + coll_name: collection.name, + db_name: database.name, + session: session, + batch_size: batch_size, + to_return: 0, + # max_time_ms is not being passed here, I assume intentionally? + } + op = Operation::GetMore.new(spec) + result = op.execute(server, context: Operation::Context.new(client: client, session: session)) Cursor.new(self, result, server, session: session) end end diff --git a/lib/mongo/cursor.rb b/lib/mongo/cursor.rb index b5caa47e04..5865b530e3 100644 --- a/lib/mongo/cursor.rb +++ b/lib/mongo/cursor.rb @@ -369,11 +369,24 @@ def cache_batch_resume_token end def get_more_operation - if @server.with_connection { |connection| connection.features }.find_command_enabled? - spec = Builder::GetMoreCommand.new(self, @session).specification - else - spec = Builder::OpGetMore.new(self).specification - end + spec = { + session: @session, + db_name: database.name, + coll_name: collection_name, + cursor_id: id, + # 3.2+ servers use batch_size, 3.0- servers use to_return. + # TODO should to_return be calculated in the operation layer? + batch_size: batch_size, + to_return: to_return, + max_time_ms: if view.respond_to?(:max_await_time_ms) && + view.max_await_time_ms && + view.options[:await_data] + then + view.max_await_time_ms + else + nil + end, + } Operation::GetMore.new(spec) end diff --git a/lib/mongo/cursor/builder.rb b/lib/mongo/cursor/builder.rb index a821682c7d..f5a948ff60 100644 --- a/lib/mongo/cursor/builder.rb +++ b/lib/mongo/cursor/builder.rb @@ -15,7 +15,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -require 'mongo/cursor/builder/op_get_more' require 'mongo/cursor/builder/op_kill_cursors' -require 'mongo/cursor/builder/get_more_command' require 'mongo/cursor/builder/kill_cursors_command' diff --git a/lib/mongo/cursor/builder/get_more_command.rb b/lib/mongo/cursor/builder/get_more_command.rb deleted file mode 100644 index 1e2170cd5c..0000000000 --- a/lib/mongo/cursor/builder/get_more_command.rb +++ /dev/null @@ -1,80 +0,0 @@ -# frozen_string_literal: true -# encoding: utf-8 - -# Copyright (C) 2015-2020 MongoDB Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -module Mongo - class Cursor - module Builder - - # Generates a specification for a get more command. - # - # @since 2.2.0 - class GetMoreCommand - extend Forwardable - - # @return [ Cursor ] cursor The cursor. - attr_reader :cursor - - def_delegators :@cursor, :collection_name, :database, :view - def_delegators :view, :batch_size - - # Create the new builder. - # - # @example Create the builder. - # GetMoreCommand.new(cursor) - # - # @param [ Cursor ] cursor The cursor. - # @param [ Session ] session The session. - # - # @since 2.2.0 - def initialize(cursor, session = nil) - @cursor = cursor - @session = session - end - - # Get the specification. - # - # @example Get the specification. - # get_more_command.specification - # - # @return [ Hash ] The spec. - # - # @since 2.2.0 - def specification - { selector: get_more_command, db_name: database.name, session: @session } - end - - private - - def get_more_command - command = { - :getMore => BSON::Int64.new(cursor.id), - :collection => collection_name, - } - command[:batchSize] = batch_size.abs if batch_size && batch_size != 0 - # If the max_await_time_ms option is set, then we set maxTimeMS on - # the get more command. - if view.respond_to?(:max_await_time_ms) - if view.max_await_time_ms && view.options[:await_data] - command[:maxTimeMS] = view.max_await_time_ms - end - end - command - end - end - end - end -end diff --git a/lib/mongo/cursor/builder/op_get_more.rb b/lib/mongo/cursor/builder/op_get_more.rb deleted file mode 100644 index af5da4973b..0000000000 --- a/lib/mongo/cursor/builder/op_get_more.rb +++ /dev/null @@ -1,64 +0,0 @@ -# frozen_string_literal: true -# encoding: utf-8 - -# Copyright (C) 2015-2020 MongoDB Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -module Mongo - class Cursor - module Builder - - # Encapsulates behavior around generating an OP_GET_MORE specification. - # - # @since 2.2.0 - class OpGetMore - extend Forwardable - - # @return [ Cursor ] cursor The cursor. - attr_reader :cursor - - def_delegators :@cursor, :collection_name, :database, :to_return - - # Create the new builder. - # - # @example Create the builder. - # OpGetMore.new(cursor) - # - # @param [ Cursor ] cursor The cursor. - # - # @since 2.2.0 - def initialize(cursor) - @cursor = cursor - end - - # Get the specification. - # - # @example Get the specification. - # op_get_more.specification - # - # @return [ Hash ] The specification. - # - # @since 2.2.0 - def specification - { - :to_return => to_return, - :cursor_id => BSON::Int64.new(cursor.id), - :db_name => database.name, - :coll_name => collection_name, - } - end - end - end - end -end diff --git a/lib/mongo/operation.rb b/lib/mongo/operation.rb index 1160a69f43..26deeef425 100644 --- a/lib/mongo/operation.rb +++ b/lib/mongo/operation.rb @@ -54,6 +54,14 @@ module Mongo + # This module encapsulates all of the operation classes defined by the driver. + # + # The operation classes take Ruby options as constructor parameters. + # For example, :read contains read preference and :read_concern contains read + # concern, whereas server commands use readConcern field for the read + # concern and read preference is passed as $readPreference or secondaryOk + # wire protocol flag bit. + # # @api private module Operation diff --git a/lib/mongo/operation/context.rb b/lib/mongo/operation/context.rb index 29f3a6e24a..9cefdf65a5 100644 --- a/lib/mongo/operation/context.rb +++ b/lib/mongo/operation/context.rb @@ -24,6 +24,15 @@ module Operation # in a single container, and provides facade methods for the contained # objects. # + # The context contains parameters for operations, and as such while an + # operation is being prepared nothing in the context should change. + # When the result of the operation is being processed, the data + # returned by the context may change (for example, because a transaction + # is aborted), but at that point the operation should no longer read + # anything from the context. Because context data may change during + # operation execution, context objects should not be reused for multiple + # operations. + # # @api private class Context def initialize(client: nil, session: nil, options: nil) diff --git a/lib/mongo/operation/explain/command.rb b/lib/mongo/operation/explain/command.rb index 1fedc83e1d..b497ed3ef9 100644 --- a/lib/mongo/operation/explain/command.rb +++ b/lib/mongo/operation/explain/command.rb @@ -34,7 +34,14 @@ class Command private def selector(connection) - super.merge(spec[:explain]) + # The mappings are BSON::Documents and as such store keys as + # strings, the spec here has symbol keys. + spec = BSON::Document.new(self.spec) + { + explain: { + find: coll_name, + }.update(Find::Builder::Command.selector(spec, connection)), + }.update(spec[:explain] || {}) end def message(connection) diff --git a/lib/mongo/operation/explain/legacy.rb b/lib/mongo/operation/explain/legacy.rb index a1496d8939..45a962d978 100644 --- a/lib/mongo/operation/explain/legacy.rb +++ b/lib/mongo/operation/explain/legacy.rb @@ -32,12 +32,15 @@ class Legacy private - def selector(connection) - super.merge(spec[:explain]) - end - def message(connection) - Protocol::Query.new(db_name, coll_name, command(connection), options(connection)) + Protocol::Query.new( + db_name, + coll_name, + Find::Builder::Legacy.selector(spec, connection), + options(connection).update( + Find::Builder::Legacy.query_options(spec, connection), + ), + ) end end end diff --git a/lib/mongo/operation/explain/op_msg.rb b/lib/mongo/operation/explain/op_msg.rb index 93db9a0eee..1500045e20 100644 --- a/lib/mongo/operation/explain/op_msg.rb +++ b/lib/mongo/operation/explain/op_msg.rb @@ -32,7 +32,15 @@ class OpMsg < OpMsgBase private def selector(connection) - super.merge(spec[:explain]) + # The mappings are BSON::Documents and as such store keys as + # strings, the spec here has symbol keys. + spec = BSON::Document.new(self.spec) + { + explain: { + find: coll_name, + }.update(Find::Builder::Command.selector(spec, connection)), + Protocol::Msg::DATABASE_IDENTIFIER => db_name, + }.update(spec[:explain] || {}) end end end diff --git a/lib/mongo/operation/find.rb b/lib/mongo/operation/find.rb index 6c2540ec35..a4528c015a 100644 --- a/lib/mongo/operation/find.rb +++ b/lib/mongo/operation/find.rb @@ -19,6 +19,7 @@ require 'mongo/operation/find/op_msg' require 'mongo/operation/find/legacy' require 'mongo/operation/find/result' +require 'mongo/operation/find/builder' module Mongo module Operation diff --git a/lib/mongo/operation/find/builder.rb b/lib/mongo/operation/find/builder.rb new file mode 100644 index 0000000000..c1d8b1fea6 --- /dev/null +++ b/lib/mongo/operation/find/builder.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true +# encoding: utf-8 + +# Copyright (C) 2015-2020 MongoDB Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +require 'mongo/operation/find/builder/command' +require 'mongo/operation/find/builder/flags' +require 'mongo/operation/find/builder/legacy' +require 'mongo/operation/find/builder/modifiers' diff --git a/lib/mongo/operation/find/builder/command.rb b/lib/mongo/operation/find/builder/command.rb new file mode 100644 index 0000000000..44172c3792 --- /dev/null +++ b/lib/mongo/operation/find/builder/command.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true +# encoding: utf-8 + +# Copyright (C) 2015-2020 MongoDB Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + module Operation + class Find + module Builder + + # Builds a find command specification from options. + # + # @api private + module Command + + # The mappings from ruby options to the find command. + OPTION_MAPPINGS = BSON::Document.new( + allow_disk_use: 'allowDiskUse', + allow_partial_results: 'allowPartialResults', + await_data: 'awaitData', + batch_size: 'batchSize', + collation: 'collation', + comment: 'comment', + filter: 'filter', + hint: 'hint', + limit: 'limit', + max_scan: 'maxScan', + max_time_ms: 'maxTimeMS', + max_value: 'max', + min_value: 'min', + no_cursor_timeout: 'noCursorTimeout', + oplog_replay: 'oplogReplay', + projection: 'projection', + read_concern: 'readConcern', + return_key: 'returnKey', + show_disk_loc: 'showRecordId', + single_batch: 'singleBatch', + skip: 'skip', + snapshot: 'snapshot', + sort: 'sort', + tailable: 'tailable', + tailable_cursor: 'tailable', + ).freeze + + module_function def selector(spec, connection) + BSON::Document.new.tap do |selector| + OPTION_MAPPINGS.each do |k, server_k| + unless (value = spec[k]).nil? + selector[server_k] = value + end + end + + if rc = selector[:readConcern] + selector[:readConcern] = Options::Mapper.transform_values_to_strings(rc) + end + + convert_limit_and_batch_size!(selector) + end + end + + private + + # Converts negative limit and batchSize parameters in the + # find command to positive ones. Removes the parameters if their + # values are zero. + # + # This is only used for find commmand, not for OP_QUERY path. + # + # The +command+ parameter is mutated by this method. + module_function def convert_limit_and_batch_size!(command) + if command[:limit] && command[:limit] < 0 && + command[:batchSize] && command[:batchSize] < 0 + then + command[:limit] = command[:limit].abs + command[:batchSize] = command[:limit].abs + command[:singleBatch] = true + else + [:limit, :batchSize].each do |opt| + if command[opt] + if command[opt] < 0 + command[opt] = command[opt].abs + command[:singleBatch] = true + elsif command[opt] == 0 + command.delete(opt) + end + end + end + end + end + end + end + end + end +end diff --git a/lib/mongo/collection/view/builder/flags.rb b/lib/mongo/operation/find/builder/flags.rb similarity index 77% rename from lib/mongo/collection/view/builder/flags.rb rename to lib/mongo/operation/find/builder/flags.rb index 31d3eb7933..c7820d61f9 100644 --- a/lib/mongo/collection/view/builder/flags.rb +++ b/lib/mongo/operation/find/builder/flags.rb @@ -16,19 +16,17 @@ # limitations under the License. module Mongo - class Collection - class View + module Operation + class Find module Builder - # Provides behavior for mapping flags. + # Provides behavior for converting Ruby options to wire protocol flags + # when sending find and related commands (e.g. explain). # - # @since 2.2.0 + # @api private module Flags - extend self # Options to cursor flags mapping. - # - # @since 2.2.0 MAPPINGS = { :allow_partial_results => [ :partial ], :oplog_replay => [ :oplog_replay ], @@ -36,20 +34,18 @@ module Flags :tailable => [ :tailable_cursor ], :tailable_await => [ :await_data, :tailable_cursor], :await_data => [ :await_data ], - :exhaust => [ :exhaust ] + :exhaust => [ :exhaust ], }.freeze - # Maps an array of flags from the provided options. + # Converts Ruby find options to an array of flags. # - # @example Map the flags. - # Flags.map_flags(options) + # Any keys in the input hash that are not options that map to flags + # are ignored. # # @param [ Hash, BSON::Document ] options The options. # # @return [ Array ] The flags. - # - # @since 2.2.0 - def map_flags(options) + module_function def map_flags(options) MAPPINGS.each.reduce(options[:flags] || []) do |flags, (key, value)| cursor_type = options[:cursor_type] if options[key] || (cursor_type && cursor_type == key) diff --git a/lib/mongo/operation/find/builder/legacy.rb b/lib/mongo/operation/find/builder/legacy.rb new file mode 100644 index 0000000000..769d3d8dd0 --- /dev/null +++ b/lib/mongo/operation/find/builder/legacy.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true +# encoding: utf-8 + +# Copyright (C) 2015-2020 MongoDB Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + module Operation + class Find + module Builder + + # Builds a legacy OP_QUERY specification from options. + # + # @api private + module Legacy + + # Mappings from driver options to legacy server values. + # + # @since 2.2.0 + DRIVER_MAPPINGS = { + comment: '$comment', + explain: '$explain', + hint: '$hint', + max_scan: '$maxScan', + max_time_ms: '$maxTimeMS', + max_value: '$max', + min_value: '$min', + show_disk_loc: '$showDiskLoc', + snapshot: '$snapshot', + sort: '$orderby', + return_key: '$returnKey', + }.freeze + + module_function def selector(spec, connection) + if Lint.enabled? + if spec.keys.any? { |k| String === k } + raise Error::LintError, "The spec must contain symbol keys only" + end + end + + # Server versions that do not have the find command feature + # (versions older than 3.2) do not support the allow_disk_use option + # but perform no validation and will not raise an error if it is + # specified. If the allow_disk_use option is specified, raise an error + # to alert the user. + unless spec[:allow_disk_use].nil? + raise Error::UnsupportedOption.allow_disk_use_error + end + + modifiers = {} + DRIVER_MAPPINGS.each do |k, server_k| + unless (value = spec[k]).nil? + modifiers[server_k] = value + end + end + + selector = spec[:filter] || BSON::Document.new + # Write nil into rp if not talking to mongos, rather than false + rp = if connection.description.mongos? + read_pref_formatted(spec) + end + if modifiers.any? || rp + selector = {'$query' => selector}.update(modifiers) + + if rp + selector['$readPreference'] = rp + end + end + + selector + end + + module_function def query_options(spec, connection) + query_options = { + project: spec[:projection], + skip: spec[:skip], + limit: spec[:limit], + # batch_size is converted to batchSize by Mongo::Protocol::Query. + batch_size: spec[:batch_size], + } + + unless (flags = Builder::Flags.map_flags(spec)).empty? + query_options[:flags] = ((query_options[:flags] || []) + flags).uniq + end + + query_options + end + + private + + module_function def read_pref_formatted(spec) + if spec[:read_preference] + raise ArgumentError, "Spec cannot include :read_preference here, use :read" + end + + if read = spec[:read] + read_pref = ServerSelector.get(read).to_mongos + Mongo::Lint.validate_camel_case_read_preference(read_pref) + read_pref + else + nil + end + end + end + end + end + end +end diff --git a/lib/mongo/collection/view/builder/modifiers.rb b/lib/mongo/operation/find/builder/modifiers.rb similarity index 59% rename from lib/mongo/collection/view/builder/modifiers.rb rename to lib/mongo/operation/find/builder/modifiers.rb index e9cc3689fe..4aec9389f5 100644 --- a/lib/mongo/collection/view/builder/modifiers.rb +++ b/lib/mongo/operation/find/builder/modifiers.rb @@ -16,64 +16,70 @@ # limitations under the License. module Mongo - class Collection - class View + module Operation + class Find module Builder - # Provides behavior for mapping modifiers. + # Provides behavior for mapping Ruby options to legacy OP_QUERY + # find modifiers. # - # @since 2.2.0 + # This module is used in two ways: + # 1. When Collection#find is invoked with the legacy OP_QUERY + # syntax (:$query argument etc.), this module is used to map + # the legacy parameters into the Ruby options that normally + # are used by applications. + # 2. When sending a find operation using the OP_QUERY protocol, + # this module is used to map the Ruby find options to the + # modifiers in the wire protocol message. + # + # @api private module Modifiers - extend self - # Mappings from driver options to legacy server values. - # - # @since 2.2.0 + # Mappings from Ruby options to OP_QUERY modifiers. DRIVER_MAPPINGS = BSON::Document.new( - sort: '$orderby', - hint: '$hint', comment: '$comment', - snapshot: '$snapshot', + explain: '$explain', + hint: '$hint', max_scan: '$maxScan', + max_time_ms: '$maxTimeMS', max_value: '$max', min_value: '$min', - max_time_ms: '$maxTimeMS', return_key: '$returnKey', show_disk_loc: '$showDiskLoc', - explain: '$explain' + snapshot: '$snapshot', + sort: '$orderby', ).freeze - # Mappings from server values to driver options. - # - # @since 2.2.0 + # Mappings from OP_QUERY modifiers to Ruby options. SERVER_MAPPINGS = BSON::Document.new(DRIVER_MAPPINGS.invert).freeze - # Transform the provided server modifiers to driver options. + # Transform the provided OP_QUERY modifiers to Ruby options. # # @example Transform to driver options. # Modifiers.map_driver_options(modifiers) # # @param [ Hash ] modifiers The modifiers. # - # @return [ BSON::Document ] The driver options. - # - # @since 2.2.0 - def self.map_driver_options(modifiers) + # @return [ BSON::Document ] The Ruby options. + module_function def map_driver_options(modifiers) Options::Mapper.transform_documents(modifiers, SERVER_MAPPINGS) end - # Transform the provided options into a document of only server + # Transform the provided Ruby options into a document of OP_QUERY # modifiers. # + # Accepts both string and symbol keys. + # + # The input mapping may contain additional keys that do not map to + # OP_QUERY modifiers, in which case the extra keys are ignored. + # # @example Map the server modifiers. # Modifiers.map_server_modifiers(options) # # @param [ Hash, BSON::Document ] options The options. # # @return [ BSON::Document ] The modifiers. - # - # @since 2.2.0 - def self.map_server_modifiers(options) + module_function def map_server_modifiers(options) Options::Mapper.transform_documents(options, DRIVER_MAPPINGS) end end diff --git a/lib/mongo/operation/find/command.rb b/lib/mongo/operation/find/command.rb index 38a277d62c..181b557c10 100644 --- a/lib/mongo/operation/find/command.rb +++ b/lib/mongo/operation/find/command.rb @@ -33,6 +33,15 @@ class Command private + def selector(connection) + # The mappings are BSON::Documents and as such store keys as + # strings, the spec here has symbol keys + spec = BSON::Document.new(self.spec) + { + find: coll_name, + }.update(Find::Builder::Command.selector(spec, connection)) + end + def message(connection) Protocol::Query.new(db_name, Database::COMMAND, command(connection), options(connection)) end diff --git a/lib/mongo/operation/find/legacy.rb b/lib/mongo/operation/find/legacy.rb index f219b6094e..33989d1b8d 100644 --- a/lib/mongo/operation/find/legacy.rb +++ b/lib/mongo/operation/find/legacy.rb @@ -35,7 +35,16 @@ class Legacy private def message(connection) - Protocol::Query.new(db_name, coll_name, command(connection), options(connection)) + selector = Find::Builder::Legacy.selector(spec, connection) + options = options(connection).update( + Find::Builder::Legacy.query_options(spec, connection), + ) + Protocol::Query.new( + db_name, + coll_name, + selector, + options, + ) end end end diff --git a/lib/mongo/operation/find/op_msg.rb b/lib/mongo/operation/find/op_msg.rb index 7eab6b06b3..9ebe85c60e 100644 --- a/lib/mongo/operation/find/op_msg.rb +++ b/lib/mongo/operation/find/op_msg.rb @@ -28,6 +28,18 @@ class OpMsg < OpMsgBase include CausalConsistencySupported include ExecutableTransactionLabel include PolymorphicResult + + private + + def selector(connection) + # The mappings are BSON::Documents and as such store keys as + # strings, the spec here has symbol keys. + spec = BSON::Document.new(self.spec) + { + find: coll_name, + Protocol::Msg::DATABASE_IDENTIFIER => db_name, + }.update(Find::Builder::Command.selector(spec, connection)) + end end end end diff --git a/lib/mongo/operation/get_more.rb b/lib/mongo/operation/get_more.rb index d7db7ef731..455f8f6394 100644 --- a/lib/mongo/operation/get_more.rb +++ b/lib/mongo/operation/get_more.rb @@ -15,6 +15,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +require 'mongo/operation/get_more/command_builder' require 'mongo/operation/get_more/command' require 'mongo/operation/get_more/op_msg' require 'mongo/operation/get_more/legacy' diff --git a/lib/mongo/operation/get_more/command.rb b/lib/mongo/operation/get_more/command.rb index 29c94c7eed..9188d20e10 100644 --- a/lib/mongo/operation/get_more/command.rb +++ b/lib/mongo/operation/get_more/command.rb @@ -30,6 +30,7 @@ class Command include Limited include ReadPreferenceSupported include PolymorphicResult + include CommandBuilder private diff --git a/lib/mongo/operation/get_more/command_builder.rb b/lib/mongo/operation/get_more/command_builder.rb new file mode 100644 index 0000000000..2ee7d43c4a --- /dev/null +++ b/lib/mongo/operation/get_more/command_builder.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +# encoding: utf-8 + +# Copyright (C) 2021 MongoDB Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +module Mongo + module Operation + class GetMore + + # @api private + module CommandBuilder + + private + + def selector(connection) + Utils.compact_hash( + getMore: BSON::Int64.new(spec.fetch(:cursor_id)), + collection: spec.fetch(:coll_name), + batchSize: spec[:batch_size], + maxTimeMS: spec[:max_time_ms], + ) + end + end + end + end +end diff --git a/lib/mongo/operation/get_more/op_msg.rb b/lib/mongo/operation/get_more/op_msg.rb index b0fb66858f..c0a4dda3d4 100644 --- a/lib/mongo/operation/get_more/op_msg.rb +++ b/lib/mongo/operation/get_more/op_msg.rb @@ -27,6 +27,7 @@ class GetMore class OpMsg < OpMsgBase include ExecutableTransactionLabel include PolymorphicResult + include CommandBuilder end end end diff --git a/lib/mongo/operation/map_reduce/op_msg.rb b/lib/mongo/operation/map_reduce/op_msg.rb index f4837a1926..80698032c4 100644 --- a/lib/mongo/operation/map_reduce/op_msg.rb +++ b/lib/mongo/operation/map_reduce/op_msg.rb @@ -19,7 +19,7 @@ module Mongo module Operation class MapReduce - # A MongoDB mapreduce operation sent as an op message. + # A MongoDB map-reduce operation sent as an op message. # # @api private # diff --git a/lib/mongo/operation/shared/sessions_supported.rb b/lib/mongo/operation/shared/sessions_supported.rb index e111fba839..d2328e642d 100644 --- a/lib/mongo/operation/shared/sessions_supported.rb +++ b/lib/mongo/operation/shared/sessions_supported.rb @@ -133,7 +133,7 @@ def command(connection) end end - sel = selector(connection).dup + sel = BSON::Document.new(selector(connection)) add_write_concern!(sel) sel[Protocol::Msg::DATABASE_IDENTIFIER] = db_name diff --git a/lib/mongo/operation/shared/specifiable.rb b/lib/mongo/operation/shared/specifiable.rb index 3373de5378..2dd7164eb0 100644 --- a/lib/mongo/operation/shared/specifiable.rb +++ b/lib/mongo/operation/shared/specifiable.rb @@ -238,7 +238,7 @@ def documents # # @since 2.0.0 def coll_name - spec[COLL_NAME] + spec.fetch(COLL_NAME) end # The id of the cursor created on the server. diff --git a/lib/mongo/protocol/query.rb b/lib/mongo/protocol/query.rb index e4e3f19115..49988cb903 100644 --- a/lib/mongo/protocol/query.rb +++ b/lib/mongo/protocol/query.rb @@ -64,13 +64,21 @@ class Query < Message def initialize(database, collection, selector, options = {}) @database = database @namespace = "#{database}.#{collection}" + if selector.nil? + raise ArgumentError, 'Selector cannot be nil' + end @selector = selector @options = options @project = options[:project] @limit = determine_limit @skip = options[:skip] || 0 @flags = options[:flags] || [] - @upconverter = Upconverter.new(collection, selector, options, flags) + @upconverter = Upconverter.new( + collection, + BSON::Document.new(selector), + BSON::Document.new(options), + flags, + ) super end @@ -224,7 +232,7 @@ class Upconverter }.freeze SPECIAL_FIELD_MAPPINGS = { - :$readPreference => 'readPreference', + :$readPreference => '$readPreference', :$orderby => 'sort', :$hint => 'hint', :$comment => 'comment', @@ -249,16 +257,6 @@ class Upconverter :partial => 'allowPartialResults' }.freeze - # Find command constant. - # - # @since 2.1.0 - FIND = 'find'.freeze - - # Filter attribute constant. - # - # @since 2.1.0 - FILTER = 'filter'.freeze - # @return [ String ] collection The name of the collection. attr_reader :collection @@ -283,6 +281,15 @@ class Upconverter # # @since 2.1.0 def initialize(collection, filter, options, flags) + # Although the docstring claims both hashes and BSON::Documents + # are acceptable, this class expects the filter and options to + # contain symbol keys which isn't what the operation layer produces. + unless BSON::Document === filter + raise ArgumentError, 'Filter must provide indifferent access' + end + unless BSON::Document === options + raise ArgumentError, 'Options must provide indifferent access' + end @collection = collection @filter = filter @options = options @@ -311,7 +318,7 @@ def command # # @since 2.1.0 def command_name - ((filter[:$query] || !command?) ? FIND : filter.keys.first).to_s + ((filter[:$query] || !command?) ? :find : filter.keys.first).to_s end private @@ -333,15 +340,25 @@ def op_command end def find_command - document = BSON::Document.new - document.store(FIND, collection) - document.store(FILTER, query_filter) + document = BSON::Document.new( + find: collection, + filter: query_filter, + ) OPTION_MAPPINGS.each do |legacy, option| document.store(option, options[legacy]) unless options[legacy].nil? end - Mongo::Lint.validate_camel_case_read_preference(filter['readPreference']) + if Lint.enabled? + filter.each do |k, v| + unless String === k + raise Error::LintError, "All keys in filter must be strings: #{filter.inspect}" + end + end + end + Lint.validate_camel_case_read_preference(filter['readPreference']) SPECIAL_FIELD_MAPPINGS.each do |special, normal| - document.store(normal, filter[special]) unless filter[special].nil? + unless (v = filter[special]).nil? + document.store(normal, v) + end end FLAG_MAPPINGS.each do |legacy, flag| document.store(flag, true) if flags.include?(legacy) diff --git a/lib/mongo/utils.rb b/lib/mongo/utils.rb index 6583ad0889..e90a8acfd1 100644 --- a/lib/mongo/utils.rb +++ b/lib/mongo/utils.rb @@ -102,5 +102,12 @@ def initialize(**opts) Process.clock_gettime(Process::CLOCK_MONOTONIC) end + # Hash#compact implementation for Ruby 2.3 + module_function def compact_hash(hash) + Hash[hash.reject do |k, v| + v.nil? + end] + end + end end diff --git a/spec/integration/crud_spec.rb b/spec/integration/crud_spec.rb index f0fc9ffad8..876f934a23 100644 --- a/spec/integration/crud_spec.rb +++ b/spec/integration/crud_spec.rb @@ -4,7 +4,8 @@ require 'spec_helper' describe 'CRUD operations' do - let(:collection) { authorized_client['crud_integration'] } + let(:client) { authorized_client } + let(:collection) { client['crud_integration'] } before do collection.delete_many @@ -64,6 +65,178 @@ end end end + + context 'with read concern' do + # Read concern requires 3.2+ server. + min_server_fcv '3.2' + + context 'with read concern specified on operation level' do + + it 'passes the read concern' do + event = Utils.get_command_event(client, 'find') do |client| + client['foo'].find({}, read_concern: {level: :local}).to_a + end + event.command.fetch('readConcern').should == {'level' => 'local'} + end + end + + context 'with read concern specified on collection level' do + + it 'passes the read concern' do + event = Utils.get_command_event(client, 'find') do |client| + client['foo', read_concern: {level: :local}].find.to_a + end + event.command.fetch('readConcern').should == {'level' => 'local'} + end + end + + context 'with read concern specified on client level' do + + let(:client) { authorized_client.with(read_concern: {level: :local}) } + + it 'passes the read concern' do + event = Utils.get_command_event(client, 'find') do |client| + client['foo'].find.to_a + end + event.command.fetch('readConcern').should == {'level' => 'local'} + end + end + end + + context 'with oplog_replay option' do + let(:collection_name) { 'crud_integration_oplog_replay' } + + let(:oplog_query) do + {ts: {'$gt' => 1}} + end + + context 'passed to operation' do + it 'passes the option' do + event = Utils.get_command_event(client, 'find') do |client| + client[collection_name].find(oplog_query, oplog_replay: true).to_a + end + event.command.fetch('oplogReplay').should be true + end + + it 'warns' do + client.should receive(:log_warn).with('The :oplog_replay option is deprecated and ignored by MongoDB 4.4 and later') + client[collection_name].find(oplog_query, oplog_replay: true).to_a + end + end + + context 'set on collection' do + it 'passes the option' do + event = Utils.get_command_event(client, 'find') do |client| + client[collection_name, oplog_replay: true].find(oplog_query).to_a + end + event.command.fetch('oplogReplay').should be true + end + + it 'warns' do + client.should receive(:log_warn).with('The :oplog_replay option is deprecated and ignored by MongoDB 4.4 and later') + client[collection_name, oplog_replay: true].find(oplog_query).to_a + end + end + end + end + + describe 'explain' do + context 'with explicit session' do + min_server_fcv '3.6' + + it 'passes the session' do + client.start_session do |session| + event = Utils.get_command_event(client, 'explain') do |client| + client['foo'].find({}, session: session).explain.should be_explain_output + end + event.command.fetch('lsid').should == session.session_id + end + end + end + + context 'with read preference specified on operation level' do + require_topology :sharded + + # RUBY-2706 + min_server_fcv '3.6' + + it 'passes the read preference' do + event = Utils.get_command_event(client, 'explain') do |client| + client['foo'].find({}, read: {mode: :secondary_preferred}).explain.should be_explain_output + end + event.command.fetch('$readPreference').should == {'mode' => 'secondaryPreferred'} + end + end + + context 'with read preference specified on collection level' do + require_topology :sharded + + # RUBY-2706 + min_server_fcv '3.6' + + it 'passes the read preference' do + event = Utils.get_command_event(client, 'explain') do |client| + client['foo', read: {mode: :secondary_preferred}].find.explain.should be_explain_output + end + event.command.fetch('$readPreference').should == {'mode' => 'secondaryPreferred'} + end + end + + context 'with read preference specified on client level' do + require_topology :sharded + + # RUBY-2706 + min_server_fcv '3.6' + + let(:client) { authorized_client.with(read: {mode: :secondary_preferred}) } + + it 'passes the read preference' do + event = Utils.get_command_event(client, 'explain') do |client| + client['foo'].find.explain.should be_explain_output + end + event.command.fetch('$readPreference').should == {'mode' => 'secondaryPreferred'} + end + end + + context 'with read concern' do + # Read concern requires 3.2+ server. + min_server_fcv '3.2' + + context 'with read concern specifed on operation level' do + + # Read concern is not allowed in explain command, driver drops it. + it 'drops the read concern' do + event = Utils.get_command_event(client, 'explain') do |client| + client['foo'].find({}, read_concern: {level: :local}).explain.should have_key('queryPlanner') + end + event.command.should_not have_key('readConcern') + end + end + + context 'with read concern specifed on collection level' do + + # Read concern is not allowed in explain command, driver drops it. + it 'drops the read concern' do + event = Utils.get_command_event(client, 'explain') do |client| + client['foo', read_concern: {level: :local}].find.explain.should have_key('queryPlanner') + end + event.command.should_not have_key('readConcern') + end + end + + context 'with read concern specifed on client level' do + + let(:client) { authorized_client.with(read_concern: {level: :local}) } + + # Read concern is not allowed in explain command, driver drops it. + it 'drops the read concern' do + event = Utils.get_command_event(client, 'explain') do |client| + client['foo'].find.explain.should have_key('queryPlanner') + end + event.command.should_not have_key('readConcern') + end + end + end end describe 'insert' do diff --git a/spec/integration/map_reduce_spec.rb b/spec/integration/map_reduce_spec.rb new file mode 100644 index 0000000000..38afed6408 --- /dev/null +++ b/spec/integration/map_reduce_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true +# encoding: utf-8 + +require 'spec_helper' + +describe 'Map-reduce operations' do + let(:client) { authorized_client } + let(:collection) { client['mr_integration'] } + + let(:subscriber) { EventSubscriber.new } + + let(:find_options) { {} } + + let(:operation) do + collection.find({}, find_options).map_reduce('function(){}', 'function(){}') + end + + before do + collection.insert_one(test: 1) + client.subscribe(Mongo::Monitoring::COMMAND, subscriber) + end + + let(:event) { subscriber.single_command_started_event('mapReduce') } + + context 'read preference' do + require_topology :sharded + + context 'specified on client' do + let(:client) { authorized_client.with(read: {mode: :secondary_preferred }) } + + # RUBY-2706: read preference is not sent on pre-3.6 servers + min_server_fcv '3.6' + + it 'is sent' do + operation.to_a + + event.command['$readPreference'].should == {'mode' => 'secondaryPreferred'} + end + end + + context 'specified on collection' do + let(:collection) { client['mr_integration', read: {mode: :secondary_preferred }] } + + # RUBY-2706: read preference is not sent on pre-3.6 servers + min_server_fcv '3.6' + + it 'is sent' do + operation.to_a + + event.command['$readPreference'].should == {'mode' => 'secondaryPreferred'} + end + end + + context 'specified on operation' do + let(:find_options) { {read: {mode: :secondary_preferred }} } + + # RUBY-2706: read preference is not sent on pre-3.6 servers + min_server_fcv '3.6' + + it 'is sent' do + operation.to_a + + event.command['$readPreference'].should == {'mode' => 'secondaryPreferred'} + end + end + end + + context 'session' do + min_server_fcv '3.6' + + it 'is sent' do + operation.to_a + + event.command['lsid'].should_not be nil + end + end +end diff --git a/spec/mongo/collection/view/builder/find_command_spec.rb b/spec/mongo/collection/view/builder/find_command_spec.rb index 5ab7ffa924..74cc23a9a3 100644 --- a/spec/mongo/collection/view/builder/find_command_spec.rb +++ b/spec/mongo/collection/view/builder/find_command_spec.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true # encoding: utf-8 +# TODO convert, move or delete these tests as part of RUBY-2706. + +=begin require 'lite_spec_helper' describe Mongo::Collection::View::Builder::FindCommand do @@ -522,3 +525,4 @@ end end end +=end diff --git a/spec/mongo/collection/view/builder/op_query_spec.rb b/spec/mongo/collection/view/builder/op_query_spec.rb index 710ca07129..6e4b67c8d0 100644 --- a/spec/mongo/collection/view/builder/op_query_spec.rb +++ b/spec/mongo/collection/view/builder/op_query_spec.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true # encoding: utf-8 +# TODO convert, move or delete these tests as part of RUBY-2706. + +=begin require 'spec_helper' describe Mongo::Collection::View::Builder::OpQuery do @@ -155,3 +158,4 @@ end end end +=end diff --git a/spec/mongo/collection_crud_spec.rb b/spec/mongo/collection_crud_spec.rb index 0ccadbc8d2..6df6d678c3 100644 --- a/spec/mongo/collection_crud_spec.rb +++ b/spec/mongo/collection_crud_spec.rb @@ -1894,6 +1894,11 @@ def generate context 'when a session supporting causal consistency is used' do require_wired_tiger + before do + collection.drop + collection.create + end + let(:cursors) do collection.parallel_scan(2, session: session) end diff --git a/spec/mongo/cursor/builder/get_more_command_spec.rb b/spec/mongo/cursor/builder/get_more_command_spec.rb index bfec873b4b..bdffd7656e 100644 --- a/spec/mongo/cursor/builder/get_more_command_spec.rb +++ b/spec/mongo/cursor/builder/get_more_command_spec.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true # encoding: utf-8 +# TODO convert, move or delete these tests as part of RUBY-2706. + +=begin require 'spec_helper' describe Mongo::Cursor::Builder::GetMoreCommand do @@ -189,3 +192,4 @@ end end end +=end diff --git a/spec/mongo/cursor/builder/op_get_more_spec.rb b/spec/mongo/cursor/builder/op_get_more_spec.rb index 8bc864b5d2..7f4531d62a 100644 --- a/spec/mongo/cursor/builder/op_get_more_spec.rb +++ b/spec/mongo/cursor/builder/op_get_more_spec.rb @@ -1,6 +1,9 @@ # frozen_string_literal: true # encoding: utf-8 +# TODO convert, move or delete these tests as part of RUBY-2706. + +=begin require 'spec_helper' describe Mongo::Cursor::Builder::OpGetMore do @@ -62,3 +65,4 @@ end end end +=end diff --git a/spec/mongo/operation/delete/op_msg_spec.rb b/spec/mongo/operation/delete/op_msg_spec.rb index 5e1fc95856..4d0f698bae 100644 --- a/spec/mongo/operation/delete/op_msg_spec.rb +++ b/spec/mongo/operation/delete/op_msg_spec.rb @@ -95,7 +95,7 @@ context 'when write concern is specified' do it 'includes write concern in the selector' do - expect(op.send(:command, connection)[:writeConcern]).to eq(write_concern.options) + expect(op.send(:command, connection)[:writeConcern]).to eq(BSON::Document.new(write_concern.options)) end end end diff --git a/spec/mongo/collection/view/builder/flags_spec.rb b/spec/mongo/operation/find/builder/flags_spec.rb similarity index 96% rename from spec/mongo/collection/view/builder/flags_spec.rb rename to spec/mongo/operation/find/builder/flags_spec.rb index df4cd36af6..a2b480d707 100644 --- a/spec/mongo/collection/view/builder/flags_spec.rb +++ b/spec/mongo/operation/find/builder/flags_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true # encoding: utf-8 -require 'spec_helper' +require 'lite_spec_helper' -describe Mongo::Collection::View::Builder::Flags do +describe Mongo::Operation::Find::Builder::Flags do describe '.map_flags' do diff --git a/spec/mongo/collection/view/builder/modifiers_spec.rb b/spec/mongo/operation/find/builder/modifiers_spec.rb similarity index 98% rename from spec/mongo/collection/view/builder/modifiers_spec.rb rename to spec/mongo/operation/find/builder/modifiers_spec.rb index 33966fe8e2..e1d472974f 100644 --- a/spec/mongo/collection/view/builder/modifiers_spec.rb +++ b/spec/mongo/operation/find/builder/modifiers_spec.rb @@ -1,9 +1,9 @@ # frozen_string_literal: true # encoding: utf-8 -require 'spec_helper' +require 'lite_spec_helper' -describe Mongo::Collection::View::Builder::Modifiers do +describe Mongo::Operation::Find::Builder::Modifiers do describe '.map_driver_options' do diff --git a/spec/mongo/operation/find/legacy_spec.rb b/spec/mongo/operation/find/legacy_spec.rb index b6d03245da..f9cf973f3b 100644 --- a/spec/mongo/operation/find/legacy_spec.rb +++ b/spec/mongo/operation/find/legacy_spec.rb @@ -78,7 +78,7 @@ double('description').tap do |description| expect(description).to receive(:mongos?) { false } expect(description).to receive(:standalone?) { false } - expect(description).to receive(:load_balancer?) { false } + #expect(description).to receive(:load_balancer?) { false } end end diff --git a/spec/mongo/operation/insert/op_msg_spec.rb b/spec/mongo/operation/insert/op_msg_spec.rb index 3b1fc765fd..be1274fc8b 100644 --- a/spec/mongo/operation/insert/op_msg_spec.rb +++ b/spec/mongo/operation/insert/op_msg_spec.rb @@ -95,7 +95,7 @@ context 'when write concern is specified' do it 'includes write concern in the selector' do - expect(op.send(:command, connection)[:writeConcern]).to eq(write_concern.options) + expect(op.send(:command, connection)[:writeConcern]).to eq(BSON::Document.new(write_concern.options)) end end end diff --git a/spec/mongo/operation/update/op_msg_spec.rb b/spec/mongo/operation/update/op_msg_spec.rb index 60f62a7946..144692f23d 100644 --- a/spec/mongo/operation/update/op_msg_spec.rb +++ b/spec/mongo/operation/update/op_msg_spec.rb @@ -101,7 +101,7 @@ context 'when write concern is specified' do it 'includes write concern in the selector' do - expect(op.send(:command, connection)[:writeConcern]).to eq(write_concern.options) + expect(op.send(:command, connection)[:writeConcern]).to eq(BSON::Document.new(write_concern.options)) end end end diff --git a/spec/support/matchers.rb b/spec/support/matchers.rb index 2ee77790f6..f14324c204 100644 --- a/spec/support/matchers.rb +++ b/spec/support/matchers.rb @@ -73,3 +73,16 @@ (Time.now - start_time).should < min_expected_time end end + +RSpec::Matchers.define :be_explain_output do + match do |actual| + Hash === actual && ( + actual.key?('queryPlanner') || + actual.key?('allPlans') + ) + end + + failure_message do |actual| + "expected that #{actual} is explain output: is a hash with either allPlans or queryPlanner keys present" + end +end