Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RUBY-2520 Change estimatedDocumentCount() to use the $collStats Agg Stage Instead of Count Command #2198

Merged
merged 1 commit into from Mar 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .evergreen/config.yml
Expand Up @@ -928,7 +928,7 @@ buildvariants:
tasks:
- name: "test-mlaunch"

- matrix_name: "mongo-4.9"
- matrix_name: "mongo-4.9-api-version"
matrix_spec:
auth-and-ssl: ["auth-and-ssl", "noauth-and-nossl"]
ruby: "ruby-2.7"
Expand Down
2 changes: 1 addition & 1 deletion .evergreen/config/standard.yml.erb
Expand Up @@ -131,7 +131,7 @@ buildvariants:
tasks:
- name: "test-mlaunch"

- matrix_name: "mongo-4.9"
- matrix_name: "mongo-4.9-api-version"
matrix_spec:
auth-and-ssl: ["auth-and-ssl", "noauth-and-nossl"]
ruby: "ruby-2.7"
Expand Down
48 changes: 35 additions & 13 deletions lib/mongo/collection/view/readable.rb
Expand Up @@ -230,24 +230,46 @@ def estimated_document_count(opts = {})
raise ArgumentError, "Cannot call estimated_document_count when querying with a filter"
end

cmd = { count: collection.name }
cmd[:maxTimeMS] = opts[:max_time_ms] if opts[:max_time_ms]
if read_concern
cmd[:readConcern] = Options::Mapper.transform_values_to_strings(
read_concern)
end
Mongo::Lint.validate_underscore_read_preference(opts[:read])
read_pref = opts[:read] || read_preference
selector = ServerSelector.get(read_pref || server_selector)
with_session(opts) do |session|
context = Operation::Context.new(client: client, session: session)
read_with_retry(session, selector) do |server|
Operation::Count.new(
selector: cmd,
db_name: database.name,
read: read_pref,
session: session,
).execute(server, context: Operation::Context.new(client: client, session: session))
end.n.to_i
if server.description.server_version_gte?('5.0')
pipeline = [
{'$collStats' => {'count' => {}}},
{'$group' => {'_id' => 1, 'n' => {'$sum' => '$count'}}},
]
spec = Builder::Aggregation.new(pipeline, self, options.merge(session: session)).specification
result = Operation::Aggregate.new(spec).execute(server, context: context)
result.documents.first.fetch('n')
else
cmd = { count: collection.name }
cmd[:maxTimeMS] = opts[:max_time_ms] if opts[:max_time_ms]
if read_concern
cmd[:readConcern] = Options::Mapper.transform_values_to_strings(
read_concern)
end
result = Operation::Count.new(
selector: cmd,
db_name: database.name,
read: read_pref,
session: session,
).execute(server, context: context)
result.n.to_i
end
end
end
rescue Error::OperationFailure => exc
if exc.code == 26
# NamespaceNotFound
# This should only happen with the aggregation pipeline path
# (server 4.9+). Previous servers should return 0 for nonexistent
# collections.
0
else
raise
end
end

Expand Down
3 changes: 1 addition & 2 deletions lib/mongo/operation.rb
Expand Up @@ -6,6 +6,7 @@
require 'mongo/operation/shared/executable_no_validate'
require 'mongo/operation/shared/executable_transaction_label'
require 'mongo/operation/shared/polymorphic_lookup'
require 'mongo/operation/shared/polymorphic_operation'
require 'mongo/operation/shared/polymorphic_result'
require 'mongo/operation/shared/read_preference_supported'
require 'mongo/operation/shared/bypass_document_validation'
Expand All @@ -19,8 +20,6 @@
require 'mongo/operation/shared/object_id_generator'
require 'mongo/operation/shared/op_msg_or_command'
require 'mongo/operation/shared/op_msg_or_find_command'
require 'mongo/operation/shared/op_msg_or_list_indexes_command'
require 'mongo/operation/shared/collections_info_or_list_collections'

require 'mongo/operation/op_msg_base'
require 'mongo/operation/command'
Expand Down
19 changes: 18 additions & 1 deletion lib/mongo/operation/collections_info.rb
Expand Up @@ -25,7 +25,24 @@ module Operation
# @since 2.0.0
class CollectionsInfo
include Specifiable
include CollectionsInfoOrListCollections
include PolymorphicOperation
include PolymorphicLookup

private

def final_operation(connection)
op_class = if connection.features.list_collections_enabled?
if connection.features.op_msg_enabled?
ListCollections::OpMsg
else
ListCollections::Command
end
else
CollectionsInfo::Command
end

op_class.new(spec)
end
end
end
end
16 changes: 15 additions & 1 deletion lib/mongo/operation/indexes.rb
Expand Up @@ -27,7 +27,21 @@ module Operation
# @since 2.0.0
class Indexes
include Specifiable
include OpMsgOrListIndexesCommand
include PolymorphicOperation
include PolymorphicLookup

private

def final_operation(connection)
cls = if connection.features.op_msg_enabled?
polymorphic_class(self.class.name, :OpMsg)
elsif connection.features.list_indexes_enabled?
polymorphic_class(self.class.name, :Command)
else
polymorphic_class(self.class.name, :Legacy)
end
cls.new(spec)
end
end
end
end
8 changes: 1 addition & 7 deletions lib/mongo/operation/shared/op_msg_or_command.rb
Expand Up @@ -20,15 +20,9 @@ module Operation
#
# @api private
module OpMsgOrCommand
include PolymorphicOperation
include PolymorphicLookup

def execute(server, context:, options: {})
server.with_connection do |connection|
operation = final_operation(connection)
operation.execute(connection, context: context, options: options)
end
end

private

def final_operation(connection)
Expand Down
8 changes: 1 addition & 7 deletions lib/mongo/operation/shared/op_msg_or_find_command.rb
Expand Up @@ -21,15 +21,9 @@ module Operation
#
# @api private
module OpMsgOrFindCommand
include PolymorphicOperation
include PolymorphicLookup

def execute(server, context:)
server.with_connection do |connection|
operation = final_operation(connection)
operation.execute(connection, context: context)
end
end

private

def final_operation(connection)
Expand Down
47 changes: 0 additions & 47 deletions lib/mongo/operation/shared/op_msg_or_list_indexes_command.rb

This file was deleted.

@@ -1,4 +1,4 @@
# Copyright (C) 2020 MongoDB Inc.
# 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.
Expand All @@ -15,39 +15,25 @@
module Mongo
module Operation

# Shared behavior of implementing an operation differently based on
# the server that will be executing the operation.
#
# @api private
module CollectionsInfoOrListCollections
include PolymorphicLookup
module PolymorphicOperation

# Execute the operation.
#
# @param [ Mongo::Server ] server The server to send the operation to.
# @param [ Operation::Context ] context The operation context.
# @param [ Hash ] options Operation execution options.
#
# @return [ Mongo::Operation::CollectionsInfo::Result,
# Mongo::Operation::ListCollections::Result ] The operation result.
def execute(server, context:)
# @return [ Mongo::Operation::Result ] The operation result.
def execute(server, context:, options: {})
server.with_connection do |connection|
operation = final_operation(connection)
operation.execute(connection, context: context)
operation.execute(connection, context: context, options: options)
end
end

private

def final_operation(connection)
op_class = if connection.features.list_collections_enabled?
if connection.features.op_msg_enabled?
ListCollections::OpMsg
else
ListCollections::Command
end
else
CollectionsInfo::Command
end

op_class.new(spec)
end
end
end
end
4 changes: 4 additions & 0 deletions lib/mongo/server/description.rb
Expand Up @@ -790,6 +790,10 @@ def ==(other)
# @api private
def server_version_gte?(version)
required_wv = case version
when '5.0'
12
when '4.4'
9
when '4.2'
8
when '4.0'
Expand Down
17 changes: 9 additions & 8 deletions lib/mongo/server/description/features.rb
Expand Up @@ -23,14 +23,15 @@ class Features
# List of features and the wire protocol version they appear in.
#
# Wire protocol versions map to server releases as follows:
# - 2 => 2.6
# - 3 => 3.0
# - 4 => 3.2
# - 5 => 3.4
# - 6 => 3.6
# - 7 => 4.0
# - 8 => 4.2
# - 9 => 4.4
# - 2 => 2.6
# - 3 => 3.0
# - 4 => 3.2
# - 5 => 3.4
# - 6 => 3.6
# - 7 => 4.0
# - 8 => 4.2
# - 9 => 4.4
# - 12 => 5.0
#
# @since 2.0.0
MAPPINGS = {
Expand Down
52 changes: 33 additions & 19 deletions spec/mongo/collection/view/readable_spec.rb
Expand Up @@ -604,36 +604,50 @@

describe "#estimated_document_count" do

let(:documents) do
(1..10).map{ |i| { field: "test#{i}" }}
end

before do
authorized_collection.delete_many
authorized_collection.insert_many(documents)
end

let(:result) do
view.estimated_document_count(options)
end

context 'when a selector is provided' do
context 'when collection has documents' do
let(:documents) do
(1..10).map{ |i| { field: "test#{i}" }}
end

let(:selector) do
{ field: 'test1' }
before do
authorized_collection.delete_many
authorized_collection.insert_many(documents)
end

it 'raises an error' do
expect {
result
}.to raise_error(ArgumentError, "Cannot call estimated_document_count when querying with a filter")
context 'when a selector is provided' do

let(:selector) do
{ field: 'test1' }
end

it 'raises an error' do
expect {
result
}.to raise_error(ArgumentError, "Cannot call estimated_document_count when querying with a filter")
end
end

context 'when no selector is provided' do

it 'returns the estimated count of matching documents' do
expect(view.estimated_document_count).to eq(10)
end
end
end

context 'when no selector is provided' do
context 'when collection does not exist' do

let(:view) do
Mongo::Collection::View.new(
authorized_client['nonexistent-collection-for-estimated-document-count'], selector, options)
end

it 'returns the estimated count of matching documents' do
expect(view.estimated_document_count).to eq(10)
it 'returns 0' do
view.estimated_document_count.should == 0
end
end
end
Expand Down