Refactored Query class into Cursor class.

This commit is contained in:
Kyle Banker 2009-10-22 14:10:12 -04:00
parent de5c078cec
commit 67b7f6b375
12 changed files with 295 additions and 255 deletions

View File

@ -14,11 +14,23 @@ include Config
gem_command = "gem"
gem_command = "gem1.9" if $0.match(/1\.9$/) # use gem1.9 if we used rake1.9
# NOTE: some of the tests assume Mongo is running
# NOTE: the functional tests assume MongoDB is running.
desc "Test the MongoDB Ruby driver."
Rake::TestTask.new(:test) do |t|
t.test_files = FileList['test/test*.rb']
t.verbose = true
task :test do
Rake::Task['test:unit'].invoke
Rake::Task['test:functional'].invoke
end
namespace :test do
Rake::TestTask.new(:unit) do |t|
t.test_files = FileList['test/unit/*_test.rb']
t.verbose = true
end
Rake::TestTask.new(:functional) do |t|
t.test_files = FileList['test/test*.rb']
t.verbose = true
end
end
desc "Generate documentation"

View File

@ -1,9 +1,11 @@
require 'mongo/types/binary'
require 'mongo/types/code'
require 'mongo/types/dbref'
require 'mongo/types/objectid'
require 'mongo/types/regexp_of_holding'
require 'mongo/util/conversions'
require 'mongo/util/support'
require 'mongo/errors'
require 'mongo/constants'

View File

@ -64,7 +64,7 @@ module Mongo
# Return an array contining current profiling information from the
# database.
def profiling_info
@db.query(Collection.new(@db, DB::SYSTEM_PROFILE_COLLECTION), Query.new({})).to_a
Cursor.new(Collection.new(@db, DB::SYSTEM_PROFILE_COLLECTION), :selector => {}).to_a
end
# Validate a named collection by raising an exception if there is a

View File

@ -14,8 +14,6 @@
# limitations under the License.
# ++
require 'mongo/query'
module Mongo
# A named collection of records in a database.
@ -131,7 +129,8 @@ module Mongo
end
raise RuntimeError, "Unknown options [#{options.inspect}]" unless options.empty?
cursor = @db.query(self, Query.new(selector, fields, skip, limit, sort, hint, snapshot, timeout, @db.slave_ok?))
cursor = Cursor.new(self, :selector => selector, :fields => fields, :skip => skip, :limit => limit,
:order => sort, :hint => hint, :snapshot => snapshot, :timeout => timeout)
if block_given?
yield cursor
cursor.close()

View File

@ -25,13 +25,29 @@ module Mongo
RESPONSE_HEADER_SIZE = 20
attr_reader :db, :collection, :query
attr_reader :collection, :selector, :admin, :fields,
:order, :hint, :snapshot, :timeout,
:full_collection_name
# Create a new cursor.
#
# Should not be called directly by application developers.
def initialize(db, collection, query, admin=false)
@db, @collection, @query, @admin = db, collection, query, admin
def initialize(collection, options={})
@db = collection.db
@collection = collection
@selector = convert_selector_for_query(options[:selector])
@fields = convert_fields_for_query(options[:fields])
@admin = options[:admin] || false
@skip = options[:skip] || 0
@limit = options[:limit] || 0
@order = options[:order]
@hint = options[:hint]
@snapshot = options[:snapshot]
@timeout = options[:timeout] || false
@explain = options[:explain]
@full_collection_name = "#{@collection.db.name}.#{@collection.name}"
@cache = []
@closed = false
@query_run = false
@ -64,9 +80,9 @@ module Mongo
# not take limit and skip into account. Raises OperationFailure on a
# database error.
def count
command = OrderedHash["count", @collection.name,
"query", @query.selector,
"fields", @query.fields()]
command = OrderedHash["count", @collection.name,
"query", @selector,
"fields", @fields]
response = @db.db_command(command)
return response['n'].to_i if response['ok'] == 1
return 0 if response['errmsg'] == "ns missing"
@ -93,35 +109,39 @@ module Mongo
order = key_or_list
end
@query.order_by = order
@order = order
self
end
# Limits the number of results to be returned by this cursor.
# Returns the current number_to_return if no parameter is given.
#
# Raises InvalidOperation if this cursor has already been used.
#
# This method overrides any limit specified in the Collection#find method,
# and only the last limit applied has an effect.
def limit(number_to_return)
def limit(number_to_return=nil)
return @limit unless number_to_return
check_modifiable
raise ArgumentError, "limit requires an integer" unless number_to_return.is_a? Integer
@query.number_to_return = number_to_return
@limit = number_to_return
self
end
# Skips the first +number_to_skip+ results of this cursor.
#
# Returns the current number_to_skip if no parameter is given.
#
# Raises InvalidOperation if this cursor has already been used.
#
# This method overrides any skip specified in the Collection#find method,
# and only the last skip applied has an effect.
def skip(number_to_skip)
def skip(number_to_skip=nil)
return @skip unless number_to_skip
check_modifiable
raise ArgumentError, "skip requires an integer" unless number_to_skip.is_a? Integer
@query.number_to_skip = number_to_skip
@skip = number_to_skip
self
end
@ -131,7 +151,7 @@ module Mongo
# Iterating over an entire cursor will close it.
def each
num_returned = 0
while more? && (@query.number_to_return <= 0 || num_returned < @query.number_to_return)
while more? && (@limit <= 0 || num_returned < @limit)
yield next_object()
num_returned += 1
end
@ -148,7 +168,7 @@ module Mongo
raise InvalidOperation, "can't call Cursor#to_a on a used cursor" if @query_run
rows = []
num_returned = 0
while more? && (@query.number_to_return <= 0 || num_returned < @query.number_to_return)
while more? && (@limit <= 0 || num_returned < @limit)
rows << next_object()
num_returned += 1
end
@ -157,16 +177,10 @@ module Mongo
# Returns an explain plan record for this cursor.
def explain
limit = @query.number_to_return
@query.explain = true
@query.number_to_return = -limit.abs
c = Cursor.new(@db, @collection, @query)
c = Cursor.new(@collection, query_options_hash.merge(:limit => -@limit.abs, :explain => true))
explanation = c.next_object
c.close
@query.explain = false
@query.number_to_return = limit
explanation
end
@ -194,8 +208,65 @@ module Mongo
# Returns true if this cursor is closed, false otherwise.
def closed?; @closed; end
# Returns an integer indicating which query options have been selected.
# See http://www.mongodb.org/display/DOCS/Mongo+Wire+Protocol#MongoWireProtocol-Mongo::Constants::OPQUERY
def query_opts
timeout = @timeout ? 0 : Mongo::Constants::OP_QUERY_NO_CURSOR_TIMEOUT
slave_ok = @db.slave_ok? ? Mongo::Constants::OP_QUERY_SLAVE_OK : 0
slave_ok + timeout
end
# Returns the query options set on this Cursor.
def query_options_hash
{ :selector => @selector,
:fields => @fields,
:admin => @admin,
:skip => @skip_num,
:limit => @limit_num,
:order => @order,
:hint => @hint,
:snapshot => @snapshot,
:timeout => @timeout }
end
private
# Converts the +:fields+ parameter from a single field name or an array
# of fields names to a hash, with the field names for keys and '1' for each
# value.
def convert_fields_for_query(fields)
case fields
when String, Symbol
{fields => 1}
when Array
return nil if fields.length.zero?
returning({}) do |hash|
fields.each { |field| hash[field] = 1 }
end
end
end
# Set query selector hash. If the selector is a Code or String object,
# the selector will be used in a $where clause.
# See http://www.mongodb.org/display/DOCS/Server-side+Code+Execution
def convert_selector_for_query(selector)
case selector
when Hash
selector
when nil
{}
when String
{"$where" => Code.new(selector)}
when Code
{"$where" => selector}
end
end
# Returns true if the query contains order, explain, hint, or snapshot.
def query_contains_special_fields?
@order || @explain || @hint || @snapshot
end
def read_all
read_message_header
read_response_header
@ -243,7 +314,7 @@ module Mongo
end
# Internal method, not for general use. Return +true+ if there are
# more records to retrieve. We do not check @query.number_to_return;
# more records to retrieve. This methods does not check @limit;
# #each is responsible for doing that.
def more?
num_remaining > 0
@ -308,41 +379,40 @@ module Mongo
def construct_query_message(query)
message = ByteBuffer.new
message.put_int(query.query_opts)
message.put_int(query_opts)
db_name = @admin ? 'admin' : @db.name
BSON.serialize_cstr(message, "#{db_name}.#{@collection.name}")
message.put_int(query.number_to_skip)
message.put_int(query.number_to_return)
sel = query.selector
if query.contains_special_fields
sel = add_special_query_fields(sel, query)
message.put_int(@skip)
message.put_int(@limit)
selector = @selector
if query_contains_special_fields?
selector = selector_with_special_query_fields
end
message.put_array(BSON.new.serialize(sel).to_a)
message.put_array(BSON.new.serialize(query.fields).to_a) if query.fields
message.put_array(BSON.new.serialize(selector).to_a)
message.put_array(BSON.new.serialize(@fields).to_a) if @fields
message
end
def add_special_query_fields(sel, query)
def selector_with_special_query_fields
sel = OrderedHash.new
sel['query'] = query.selector
order_by = query.order_by
sel['orderby'] = get_query_order_by(order_by) if order_by
sel['$hint'] = query.hint if query.hint && query.hint.length > 0
sel['$explain'] = true if query.explain
sel['$snapshot'] = true if query.snapshot
sel['query'] = @selector
sel['orderby'] = formatted_order_clause if @order
sel['$hint'] = @hint if @hint && @hint.length > 0
sel['$explain'] = true if @explain
sel['$snapshot'] = true if @snapshot
sel
end
def get_query_order_by(order_by)
case order_by
when String then string_as_sort_parameters(order_by)
when Symbol then symbol_as_sort_parameters(order_by)
when Array then array_as_sort_parameters(order_by)
def formatted_order_clause
case @order
when String then string_as_sort_parameters(@order)
when Symbol then symbol_as_sort_parameters(@order)
when Array then array_as_sort_parameters(@order)
when Hash # Should be an ordered hash, but this message doesn't care
warn_if_deprecated(order_by)
order_by
warn_if_deprecated(@order)
@order
else
raise InvalidSortValueError, "Illegal order_by, '#{query.order_by.class.name}'; must be String, Array, Hash, or OrderedHash"
raise InvalidSortValueError, "Illegal order_by, '#{@order.class.name}'; must be String, Array, Hash, or OrderedHash"
end
end
@ -351,7 +421,7 @@ module Mongo
end
def close_cursor_if_query_complete
close if @query.number_to_return > 0 && @n_received >= @query.number_to_return
close if @limit > 0 && @n_received >= @limit
end
def check_modifiable

View File

@ -18,7 +18,6 @@ require 'socket'
require 'digest/md5'
require 'thread'
require 'mongo/collection'
require 'mongo/query'
require 'mongo/util/ordered_hash.rb'
require 'mongo/admin'
@ -216,7 +215,7 @@ module Mongo
def collections_info(coll_name=nil)
selector = {}
selector[:name] = full_collection_name(coll_name) if coll_name
query(Collection.new(self, SYSTEM_NAMESPACE_COLLECTION), Query.new(selector))
Cursor.new(Collection.new(self, SYSTEM_NAMESPACE_COLLECTION), :selector => selector)
end
# Create a collection. If +strict+ is false, will return existing or
@ -416,7 +415,7 @@ module Mongo
def index_information(collection_name)
sel = {:ns => full_collection_name(collection_name)}
info = {}
query(Collection.new(self, SYSTEM_INDEX_COLLECTION), Query.new(sel)).each { |index|
Cursor.new(Collection.new(self, SYSTEM_INDEX_COLLECTION), :selector => sel).each { |index|
info[index['name']] = index['key'].to_a
}
info
@ -499,9 +498,8 @@ module Mongo
end
end
q = Query.new(selector)
q.number_to_return = -1
query(Collection.new(self, SYSTEM_COMMAND_COLLECTION), q, use_admin_db).next_object
cursor = Cursor.new(Collection.new(self, SYSTEM_COMMAND_COLLECTION), :admin => use_admin_db, :limit => -1, :selector => selector)
cursor.next_object
end
def _synchronize &block

View File

@ -1,132 +0,0 @@
# --
# Copyright (C) 2008-2009 10gen 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/collection'
require 'mongo/types/code'
module Mongo
# Mongo documentation for query details.
class Query
attr_accessor :number_to_skip, :number_to_return, :order_by, :snapshot
# If true, $explain will be set in QueryMessage that uses this query.
attr_accessor :explain
# Either +nil+ or a hash (preferably an OrderedHash).
attr_accessor :hint
attr_reader :selector # writer defined below
# sel :: A hash describing the query. See the Mongo docs for details.
#
# return_fields :: If not +nil+, a single field name or an array of
# field names. Only those fields will be returned.
# (Called :fields in calls to Collection#find.)
#
# number_to_skip :: Number of records to skip before returning
# records. Default is 0.
#
# number_to_return :: Max number of records to return. (Called :limit
# in calls to Collection#find.) Default is 0 (all
# records).
#
# order_by :: If not +nil+, specifies record sort order. May be a
# String, Hash, OrderedHash, or Array. If a string, the
# results will be ordered by that field in ascending
# order. If an array, it should be an array of field names
# which will all be sorted in ascending order. If a hash,
# it may be either a regular Hash or an OrderedHash. The
# keys should be field names, and the values should be 1
# (ascending) or -1 (descending). Note that if it is a
# regular Hash then sorting by more than one field
# probably will not be what you intend because key order
# is not preserved. (order_by is called :sort in calls to
# Collection#find.)
# :snapshot :: If true, snapshot mode will be used for this query.
# Snapshot mode assures no duplicates are returned, or
# objects missed, which were preset at both the start and
# end of the query's execution. For details see
# http://www.mongodb.org/display/DOCS/How+to+do+Snapshotting+in+the+Mongo+Database
#
# hint :: If not +nil+, specifies query hint fields. Must be either
# +nil+ or a hash (preferably an OrderedHash). See Collection#hint.
#
# timeout :: When +true+ (default), the returned cursor will be subject to
# the normal cursor timeout behavior of the mongod process.
# When +false+, the returned cursor will never timeout. Care should
# be taken to ensure that cursors with timeout disabled are properly closed.
def initialize(sel={}, return_fields=nil, number_to_skip=0, number_to_return=0, order_by=nil, hint=nil, snapshot=nil, timeout=true, slave_ok=false)
@number_to_skip, @number_to_return, @order_by, @hint, @snapshot, @timeout, @slave_ok =
number_to_skip, number_to_return, order_by, hint, snapshot, timeout, slave_ok
@explain = nil
self.selector = sel
self.fields = return_fields
end
# Set query selector hash. If sel is Code/string, it will be used as a
# $where clause. (See Mongo docs for details.)
def selector=(sel)
@selector = case sel
when nil
{}
when Code
{"$where" => sel}
when String
{"$where" => Code.new(sel)}
when Hash
sel
end
end
# Set fields to return. If +val+ is +nil+ or empty, all fields will be
# returned.
def fields=(val)
@fields = val
@fields = nil if @fields && @fields.empty?
end
def fields
case @fields
when String
{@fields => 1}
when Array
if @fields.length == 0
nil
else
h = {}
@fields.each { |field| h[field] = 1 }
h
end
else # nil, anything else
nil
end
end
def contains_special_fields
@order_by || @explain || @hint || @snapshot
end
# Returns an integer indicating which query options have been selected.
# See http://www.mongodb.org/display/DOCS/Mongo+Wire+Protocol#MongoWireProtocol-Mongo::Constants::OPQUERY
def query_opts
timeout = @timeout ? 0 : Mongo::Constants::OP_QUERY_NO_CURSOR_TIMEOUT
slave_ok = @slave_ok ? Mongo::Constants::OP_QUERY_SLAVE_OK : 0
slave_ok + timeout
end
def to_s
"find(#{@selector.inspect})" + (@order_by ? ".sort(#{@order_by.inspect})" : "")
end
end
end

28
lib/mongo/util/support.rb Normal file
View File

@ -0,0 +1,28 @@
# --
# Copyright (C) 2008-2009 10gen 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.
# ++
# A hash in which the order of keys are preserved.
#
# Under Ruby 1.9 and greater, this class has no added methods because Ruby's
# Hash already keeps its keys ordered by order of insertion.
class Object
def returning(value)
yield value
value
end
end

View File

@ -27,7 +27,6 @@ PACKAGE_FILES = ['README.rdoc', 'Rakefile', 'mongo-ruby-driver.gemspec',
'lib/mongo/gridfs/grid_store.rb',
'lib/mongo/gridfs.rb',
'lib/mongo/errors.rb',
'lib/mongo/query.rb',
'lib/mongo/types/binary.rb',
'lib/mongo/types/code.rb',
'lib/mongo/types/dbref.rb',
@ -37,6 +36,7 @@ PACKAGE_FILES = ['README.rdoc', 'Rakefile', 'mongo-ruby-driver.gemspec',
'lib/mongo/util/byte_buffer.rb',
'lib/mongo/util/conversions.rb',
'lib/mongo/util/ordered_hash.rb',
'lib/mongo/util/support.rb',
'lib/mongo/util/xml_to_ruby.rb',
'lib/mongo.rb']
TEST_FILES = ['test/mongo-qa/_common.rb',

View File

@ -1,54 +0,0 @@
# --
# Copyright (C) 2008-2009 10gen 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.
# ++
$LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib')
require 'mongo'
require 'test/unit'
class TestQuery < Test::Unit::TestCase
include Mongo
def test_timeout_opcodes
@timeout = true
@query = Query.new({}, nil, 0, 0, nil, nil, nil, @timeout)
assert_equal 0, @query.query_opts
@timeout = false
@query = Query.new({}, nil, 0, 0, nil, nil, nil, @timeout)
assert_equal 16, @query.query_opts
end
def test_slave_ok_opcodes
@slave_ok = true
@query = Query.new({}, nil, 0, 0, nil, nil, nil, true, @slave_ok)
assert_equal 4, @query.query_opts
@slave_ok = false
@query = Query.new({}, nil, 0, 0, nil, nil, nil, true, @slave_ok)
assert_equal 0, @query.query_opts
end
def test_combined_opcodes
@timeout = false
@slave_ok = true
@query = Query.new({}, nil, 0, 0, nil, nil, nil, @timeout, @slave_ok)
assert_equal 20, @query.query_opts
end
end

View File

@ -24,19 +24,14 @@ class SlaveConnectionTest < Test::Unit::TestCase
def test_slave_ok_sent_to_queries
@db = Connection.new(@@host, @@port, :slave_ok => true).db('ruby-mongo-demo')
@coll = @db['test-collection']
@cursor = @coll.find({})
assert_equal true, @cursor.query.instance_variable_get(:@slave_ok)
assert_equal true, @db.slave_ok?
end
else
puts "Not connected to slave; skipping slave connection tests."
def test_slave_ok_false_on_queries
@db = Connection.new(@@host, @@port).db('ruby-mongo-demo')
@coll = @db['test-collection']
@cursor = @coll.find({})
assert_nil @cursor.query.instance_variable_get(:@slave_ok)
@db = Connection.new(@@host, @@port).db('ruby-mongo-demo')
assert !@db.slave_ok?
end
end
end

122
test/unit/cursor_test.rb Normal file
View File

@ -0,0 +1,122 @@
require 'test/test_helper'
class TestCursor < Test::Unit::TestCase
context "Cursor options" do
setup do
@db = stub(:name => "testing", :slave_ok? => false)
@collection = stub(:db => @db, :name => "items")
@cursor = Cursor.new(@collection)
end
should "set admin to false" do
assert_equal false, @cursor.admin
@cursor = Cursor.new(@collection, :admin => true)
assert_equal true, @cursor.admin
end
should "set selector" do
assert @cursor.selector == {}
@cursor = Cursor.new(@collection, :selector => {:name => "Jones"})
assert @cursor.selector == {:name => "Jones"}
end
should "set fields" do
assert_nil @cursor.fields
@cursor = Cursor.new(@collection, :fields => [:name, :date])
assert @cursor.fields == {:name => 1, :date => 1}
end
should "set limit" do
assert_equal 0, @cursor.limit
@cursor = Cursor.new(@collection, :limit => 10)
assert_equal 10, @cursor.limit
end
should "set skip" do
assert_equal 0, @cursor.skip
@cursor = Cursor.new(@collection, :skip => 5)
assert_equal 5, @cursor.skip
end
should "set sort order" do
assert_nil @cursor.order
@cursor = Cursor.new(@collection, :order => "last_name")
assert_equal "last_name", @cursor.order
end
should "set hint" do
assert_nil @cursor.hint
@cursor = Cursor.new(@collection, :hint => "name")
assert_equal "name", @cursor.hint
end
should "cache full collection name" do
assert_equal "testing.items", @cursor.full_collection_name
end
end
context "Query options" do
should "test timeout true and slave_ok false" do
@db = stub(:slave_ok? => false, :name => "testing")
@collection = stub(:db => @db, :name => "items")
@cursor = Cursor.new(@collection, :timeout => true)
assert_equal 0, @cursor.query_opts
end
should "test timeout false and slave_ok false" do
@db = stub(:slave_ok? => false, :name => "testing")
@collection = stub(:db => @db, :name => "items")
@cursor = Cursor.new(@collection, :timeout => false)
assert_equal 16, @cursor.query_opts
end
should "set timeout true and slave_ok true" do
@db = stub(:slave_ok? => true, :name => "testing")
@collection = stub(:db => @db, :name => "items")
@cursor = Cursor.new(@collection, :timeout => true)
assert_equal 4, @cursor.query_opts
end
should "set timeout false and slave_ok true" do
@db = stub(:slave_ok? => true, :name => "testing")
@collection = stub(:db => @db, :name => "items")
@cursor = Cursor.new(@collection, :timeout => false)
assert_equal 20, @cursor.query_opts
end
end
context "Query fields" do
setup do
@db = stub(:slave_ok? => true, :name => "testing")
@collection = stub(:db => @db, :name => "items")
end
should "when an array should return a hash with each key" do
@cursor = Cursor.new(@collection, :fields => [:name, :age])
result = @cursor.fields
assert_equal result.keys, [:age, :name]
assert result.values.all? {|v| v == 1}
end
should "when a string, return a hash with just the key" do
@cursor = Cursor.new(@collection, :fields => "name")
result = @cursor.fields
assert_equal result.keys.sort, ["name"]
assert result.values.all? {|v| v == 1}
end
should "return nil when neither hash nor string nor symbol" do
@cursor = Cursor.new(@collection, :fields => 1234567)
assert_nil @cursor.fields
end
end
end