From 1afd968f5d865e2957caadd83ad0f6b604573b6d Mon Sep 17 00:00:00 2001 From: Jim Menard Date: Tue, 13 Jan 2009 14:02:16 -0500 Subject: [PATCH] Added Cursor#explain. Made query sends lazy. --- lib/mongo/cursor.rb | 36 +++++++++++++++++--- lib/mongo/db.rb | 15 ++++++--- lib/mongo/message/query_message.rb | 3 ++ tests/test_cursor.rb | 54 ++++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+), 9 deletions(-) create mode 100644 tests/test_cursor.rb diff --git a/lib/mongo/cursor.rb b/lib/mongo/cursor.rb index 2a35e97..fbe6a95 100644 --- a/lib/mongo/cursor.rb +++ b/lib/mongo/cursor.rb @@ -29,14 +29,19 @@ module XGen RESPONSE_HEADER_SIZE = 20 - def initialize(db, collection, num_to_return=0) - @db, @collection, @num_to_return = db, collection, num_to_return + attr_reader :db, :collection, :query_message + + def initialize(db, collection, query_message) + @db, @collection, @query_message = db, collection, query_message + @num_to_return = query_message.query.number_to_return || 0 @cache = [] @closed = false @can_call_to_a = true - read_all + @query_run = false end + def closed?; @closed; end + # Return +true+ if there are more records to retrieve. We do not check # @num_to_return; #each is responsible for doing that. def more? @@ -100,9 +105,20 @@ module XGen @rows end + # Returns an explain plan record. + def explain + sel = OrderedHash.new + sel['query'] = @query_message.query.selector + sel['$explain'] = true + c = Cursor.new(@db, @collection, QueryMessage.new(@db.name, @collection, Query.new(sel))) + e = c.next_object + c.close + e + end + # Close the cursor. def close - @db.send_to_db(KillCursorMessage(@cursor_id)) if @cursor_id + @db.send_to_db(KillCursorsMessage.new(@cursor_id)) if @cursor_id @cache = [] @cursor_id = 0 @closed = true @@ -146,6 +162,7 @@ module XGen private def next_object_on_wire + send_query_if_needed # if @n_remaining is 0 but we have a non-zero cursor, there are more # to fetch, so do a GetMore operation, but don't do it here - do it # when someone pulls an object out of the cache and it's empty @@ -154,6 +171,7 @@ module XGen end def refill_via_get_more + send_query_if_needed return if @cursor_id == 0 @db.send_to_db(GetMoreMessage.new(@db.name, @collection, @cursor_id)) read_all @@ -170,6 +188,15 @@ module XGen BSON.new(@db).deserialize(buf) end + def send_query_if_needed + # Run query first time we request an object from the wire + unless @query_run + @db.send_query_message(@query_message) + @query_run = true + read_all + end + end + def to_s "DBResponse(flags=#@result_flags, cursor_id=#@cursor_id, start=#@starting_from, n_returned=#@n_returned)" end @@ -177,4 +204,3 @@ module XGen end end end - diff --git a/lib/mongo/db.rb b/lib/mongo/db.rb index 528973f..673a011 100644 --- a/lib/mongo/db.rb +++ b/lib/mongo/db.rb @@ -177,12 +177,19 @@ module XGen send_to_db(MsgMessage.new(msg)) end - # Send a Query to +collection_name+ and return a Cursor over the - # results. + # Returns a Cursor over the query results. + # + # Note that the query gets sent lazily; the cursor calls + # #send_query_message when needed. If the caller never requests an + # object from the cursor, the query never gets sent. def query(collection_name, query) + Cursor.new(self, collection_name, QueryMessage.new(@name, collection_name, query)) + end + + # Used by a Cursor to lazily send the query to the database. + def send_query_message(query_message) @semaphore.synchronize { - send_to_db(QueryMessage.new(@name, collection_name, query)) - Cursor.new(self, collection_name, query.number_to_return) + send_to_db(query_message) } end diff --git a/lib/mongo/message/query_message.rb b/lib/mongo/message/query_message.rb index b5f8049..b7e39c7 100644 --- a/lib/mongo/message/query_message.rb +++ b/lib/mongo/message/query_message.rb @@ -8,8 +8,11 @@ module XGen class QueryMessage < Message + attr_reader :query + def initialize(db_name, collection_name, query) super(OP_QUERY) + @query = query write_int(0) write_string("#{db_name}.#{collection_name}") write_int(query.number_to_skip) diff --git a/tests/test_cursor.rb b/tests/test_cursor.rb new file mode 100644 index 0000000..6324060 --- /dev/null +++ b/tests/test_cursor.rb @@ -0,0 +1,54 @@ +$LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib') +require 'mongo' +require 'test/unit' + +# NOTE: assumes Mongo is running +class CursorTest < Test::Unit::TestCase + + include XGen::Mongo::Driver + + def setup + host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost' + port = ENV['MONGO_RUBY_DRIVER_PORT'] || Mongo::DEFAULT_PORT + @db = Mongo.new(host, port).db('ruby-mongo-test') + @coll = @db.collection('test') + @coll.clear + @r1 = @coll.insert('a' => 1) # collection not created until it's used + @coll_full_name = 'ruby-mongo-test.test' + end + + def teardown + @coll.clear unless @coll == nil || @db.socket.closed? + end + + def test_explain + cursor = @coll.find('a' => 1) + explaination = cursor.explain + assert_not_nil explaination['cursor'] + assert_kind_of Numeric, explaination['n'] + assert_kind_of Numeric, explaination['millis'] + assert_kind_of Numeric, explaination['nscanned'] + end + + def test_close_no_query_sent + begin + cursor = @coll.find('a' => 1) + cursor.close + assert cursor.closed? + rescue => ex + fail ex.to_s + end + end + + def test_close_after_query_sent + begin + cursor = @coll.find('a' => 1) + cursor.next_object + cursor.close + assert cursor.closed? + rescue => ex + fail ex.to_s + end + end + +end