# Copyright (C) 2014-2019 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/shared/result/aggregatable' require 'mongo/operation/shared/result/use_legacy_error_parser' module Mongo module Operation # Result wrapper for operations. # # @since 2.0.0 class Result extend Forwardable include Enumerable # The field name for the cursor document in an aggregation. # # @since 2.2.0 CURSOR = 'cursor'.freeze # The cursor id field in the cursor document. # # @since 2.2.0 CURSOR_ID = 'id'.freeze # The field name for the first batch of a cursor. # # @since 2.2.0 FIRST_BATCH = 'firstBatch'.freeze # The field name for the next batch of a cursor. # # @since 2.2.0 NEXT_BATCH = 'nextBatch'.freeze # The namespace field in the cursor document. # # @since 2.2.0 NAMESPACE = 'ns'.freeze # The number of documents updated in the write. # # @since 2.0.0 N = 'n'.freeze # The ok status field in the result. # # @since 2.0.0 OK = 'ok'.freeze # The result field constant. # # @since 2.2.0 RESULT = 'result'.freeze # Initialize a new result. # # @example Instantiate the result. # Result.new(replies) # # @param [ Protocol::Reply ] replies The wire protocol replies. # # @since 2.0.0 def initialize(replies) @replies = [ *replies ] if replies end # @return [ Array ] replies The wrapped wire protocol replies. attr_reader :replies # @api private def_delegators :parser, :not_master?, :node_recovering?, :node_shutting_down? # Is the result acknowledged? # # @note On MongoDB 2.6 and higher all writes are acknowledged since the # driver uses write commands for all write operations. On 2.4 and # lower, the result is acknowledged if the GLE has been executed after # the command. If not, no replies will be specified. Reads will always # return true here since a replies is always provided. # # @return [ true, false ] If the result is acknowledged. # # @since 2.0.0 def acknowledged? !!@replies end # Determine if this result is a collection of multiple replies from the # server. # # @example Is the result for multiple replies? # result.multiple? # # @return [ true, false ] If the result is for multiple replies. # # @since 2.0.0 def multiple? replies.size > 1 end # Get the cursor id if the response is acknowledged. # # @note Cursor ids of 0 indicate there is no cursor on the server. # # @example Get the cursor id. # result.cursor_id # # @return [ Integer ] The cursor id. # # @since 2.0.0 def cursor_id acknowledged? ? replies.last.cursor_id : 0 end # Get the namespace of the cursor. The method should be defined in # result classes where 'ns' is in the server response. # # @return [ Nil ] # # @since 2.0.0 def namespace nil end # Get the documents in the result. # # @example Get the documents. # result.documents # # @return [ Array ] The documents. # # @since 2.0.0 def documents if acknowledged? replies.flat_map{ |reply| reply.documents } else [] end end # Iterate over the documents in the replies. # # @example Iterate over the documents. # result.each do |doc| # p doc # end # # @return [ Enumerator ] The enumerator. # # @since 2.0.0 # # @yieldparam [ BSON::Document ] Each document in the result. def each(&block) documents.each(&block) end # Get the pretty formatted inspection of the result. # # @example Inspect the result. # result.inspect # # @return [ String ] The inspection. # # @since 2.0.0 def inspect "#<#{self.class.name}:0x#{object_id} documents=#{documents}>" end # Get the first reply from the result. # # @example Get the first reply. # result.reply # # @return [ Protocol::Reply ] The first reply. # # @since 2.0.0 def reply if acknowledged? replies.first else nil end end # Get the count of documents returned by the server. # # @example Get the number returned. # result.returned_count # # @return [ Integer ] The number of documents returned. # # @since 2.0.0 def returned_count if acknowledged? multiple? ? aggregate_returned_count : reply.number_returned else 0 end end # If the result was a command then determine if it was considered a # success. # # @note If the write was unacknowledged, then this will always return # true. # # @example Was the command successful? # result.successful? # # @return [ true, false ] If the command was successful. # # @since 2.0.0 def successful? return true if !acknowledged? if first_document.has_key?(OK) ok? && parser.message.empty? else !query_failure? && parser.message.empty? end end # Check the first document's ok field. # # @example Check the ok field. # result.ok? # # @return [ true, false ] If the command returned ok. # # @since 2.1.0 def ok? # first_document[OK] is a float, and the server can return # ok as a BSON int32, BSON int64 or a BSON double. # The number 1 is exactly representable in a float, hence # 1.0 == 1 is going to perform correctly all of the time # (until the server returns something other than 1 for success, that is) first_document[OK] == 1 end # Validate the result by checking for any errors. # # @note This only checks for errors with writes since authentication is # handled at the connection level and any authentication errors would # be raised there, before a Result is ever created. # # @example Validate the result. # result.validate! # # @raise [ Error::OperationFailure ] If an error is in the result. # # @return [ Result ] The result if verification passed. # # @since 2.0.0 def validate! !successful? ? raise_operation_failure : self end def raise_operation_failure raise Error::OperationFailure.new( parser.message, self, code: parser.code, code_name: parser.code_name, labels: parser.labels, wtimeout: parser.wtimeout) end private :raise_operation_failure # Get the number of documents written by the server. # # @example Get the number of documents written. # result.written_count # # @return [ Integer ] The number of documents written. # # @since 2.0.0 def written_count if acknowledged? multiple? ? aggregate_written_count : (first_document[N] || 0) else 0 end end alias :n :written_count # Get the operation time reported in the server response. # # @example Get the operation time. # result.operation_time # # @return [ Object ] The operation time value. # # @since 2.5.0 def operation_time first_document && first_document[OPERATION_TIME] end # Get the cluster time reported in the server response. # # @example Get the cluster time. # result.cluster_time # # @return [ BSON::Document ] The cluster time document. # # @since 2.5.0 def cluster_time first_document && first_document[CLUSTER_TIME] end # Gets the set of error labels associated with the result. # # @example Get the labels. # result.labels # # @return [ Array ] labels The set of labels. # # @since 2.7.0 def labels @labels ||= parser.labels end # Whether the operation failed with a write concern error. # # @api private def write_concern_error? !!(first_document && first_document['writeConcernError']) end private def aggregate_returned_count replies.reduce(0) do |n, reply| n += reply.number_returned n end end def aggregate_written_count documents.reduce(0) do |n, document| n += (document[N] || 0) n end end def parser @parser ||= Error::Parser.new(first_document, replies) end def first_document @first_document ||= first || BSON::Document.new end def query_failure? replies.first && (replies.first.query_failure? || replies.first.cursor_not_found?) end end end end