From 67b7f6b3758fa230edbaa412785fc4ca289b7881 Mon Sep 17 00:00:00 2001 From: Kyle Banker Date: Thu, 22 Oct 2009 14:10:12 -0400 Subject: [PATCH] Refactored Query class into Cursor class. --- Rakefile | 20 ++++- lib/mongo.rb | 2 + lib/mongo/admin.rb | 2 +- lib/mongo/collection.rb | 5 +- lib/mongo/cursor.rb | 162 ++++++++++++++++++++++++---------- lib/mongo/db.rb | 10 +-- lib/mongo/query.rb | 132 --------------------------- lib/mongo/util/support.rb | 28 ++++++ mongo-ruby-driver.gemspec | 2 +- test/test_query.rb | 54 ------------ test/test_slave_connection.rb | 11 +-- test/unit/cursor_test.rb | 122 +++++++++++++++++++++++++ 12 files changed, 295 insertions(+), 255 deletions(-) delete mode 100644 lib/mongo/query.rb create mode 100644 lib/mongo/util/support.rb delete mode 100644 test/test_query.rb create mode 100644 test/unit/cursor_test.rb diff --git a/Rakefile b/Rakefile index ebee54a..360ed75 100644 --- a/Rakefile +++ b/Rakefile @@ -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" diff --git a/lib/mongo.rb b/lib/mongo.rb index 93db0c7..b7738a0 100644 --- a/lib/mongo.rb +++ b/lib/mongo.rb @@ -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' diff --git a/lib/mongo/admin.rb b/lib/mongo/admin.rb index 25e57b5..5141efa 100644 --- a/lib/mongo/admin.rb +++ b/lib/mongo/admin.rb @@ -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 diff --git a/lib/mongo/collection.rb b/lib/mongo/collection.rb index 448c9f8..b27be8b 100644 --- a/lib/mongo/collection.rb +++ b/lib/mongo/collection.rb @@ -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() diff --git a/lib/mongo/cursor.rb b/lib/mongo/cursor.rb index 2121188..5e0eda4 100644 --- a/lib/mongo/cursor.rb +++ b/lib/mongo/cursor.rb @@ -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 diff --git a/lib/mongo/db.rb b/lib/mongo/db.rb index 7b6d116..f393fc9 100644 --- a/lib/mongo/db.rb +++ b/lib/mongo/db.rb @@ -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 diff --git a/lib/mongo/query.rb b/lib/mongo/query.rb deleted file mode 100644 index f686c2e..0000000 --- a/lib/mongo/query.rb +++ /dev/null @@ -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 diff --git a/lib/mongo/util/support.rb b/lib/mongo/util/support.rb new file mode 100644 index 0000000..7e3adde --- /dev/null +++ b/lib/mongo/util/support.rb @@ -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 diff --git a/mongo-ruby-driver.gemspec b/mongo-ruby-driver.gemspec index 2471471..f644185 100644 --- a/mongo-ruby-driver.gemspec +++ b/mongo-ruby-driver.gemspec @@ -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', diff --git a/test/test_query.rb b/test/test_query.rb deleted file mode 100644 index 0210dfa..0000000 --- a/test/test_query.rb +++ /dev/null @@ -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 diff --git a/test/test_slave_connection.rb b/test/test_slave_connection.rb index 1863c87..caf9af8 100644 --- a/test/test_slave_connection.rb +++ b/test/test_slave_connection.rb @@ -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 diff --git a/test/unit/cursor_test.rb b/test/unit/cursor_test.rb new file mode 100644 index 0000000..69b994d --- /dev/null +++ b/test/unit/cursor_test.rb @@ -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