Added Cursor#explain. Made query sends lazy.

This commit is contained in:
Jim Menard 2009-01-13 14:02:16 -05:00
parent dd48ec13ab
commit 1afd968f5d
4 changed files with 99 additions and 9 deletions

View File

@ -29,14 +29,19 @@ module XGen
RESPONSE_HEADER_SIZE = 20 RESPONSE_HEADER_SIZE = 20
def initialize(db, collection, num_to_return=0) attr_reader :db, :collection, :query_message
@db, @collection, @num_to_return = db, collection, num_to_return
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 = [] @cache = []
@closed = false @closed = false
@can_call_to_a = true @can_call_to_a = true
read_all @query_run = false
end end
def closed?; @closed; end
# Return +true+ if there are more records to retrieve. We do not check # Return +true+ if there are more records to retrieve. We do not check
# @num_to_return; #each is responsible for doing that. # @num_to_return; #each is responsible for doing that.
def more? def more?
@ -100,9 +105,20 @@ module XGen
@rows @rows
end 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. # Close the cursor.
def close def close
@db.send_to_db(KillCursorMessage(@cursor_id)) if @cursor_id @db.send_to_db(KillCursorsMessage.new(@cursor_id)) if @cursor_id
@cache = [] @cache = []
@cursor_id = 0 @cursor_id = 0
@closed = true @closed = true
@ -146,6 +162,7 @@ module XGen
private private
def next_object_on_wire def next_object_on_wire
send_query_if_needed
# if @n_remaining is 0 but we have a non-zero cursor, there are more # 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 # 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 # when someone pulls an object out of the cache and it's empty
@ -154,6 +171,7 @@ module XGen
end end
def refill_via_get_more def refill_via_get_more
send_query_if_needed
return if @cursor_id == 0 return if @cursor_id == 0
@db.send_to_db(GetMoreMessage.new(@db.name, @collection, @cursor_id)) @db.send_to_db(GetMoreMessage.new(@db.name, @collection, @cursor_id))
read_all read_all
@ -170,6 +188,15 @@ module XGen
BSON.new(@db).deserialize(buf) BSON.new(@db).deserialize(buf)
end 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 def to_s
"DBResponse(flags=#@result_flags, cursor_id=#@cursor_id, start=#@starting_from, n_returned=#@n_returned)" "DBResponse(flags=#@result_flags, cursor_id=#@cursor_id, start=#@starting_from, n_returned=#@n_returned)"
end end
@ -177,4 +204,3 @@ module XGen
end end
end end
end end

View File

@ -177,12 +177,19 @@ module XGen
send_to_db(MsgMessage.new(msg)) send_to_db(MsgMessage.new(msg))
end end
# Send a Query to +collection_name+ and return a Cursor over the # Returns a Cursor over the query results.
# 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) 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 { @semaphore.synchronize {
send_to_db(QueryMessage.new(@name, collection_name, query)) send_to_db(query_message)
Cursor.new(self, collection_name, query.number_to_return)
} }
end end

View File

@ -8,8 +8,11 @@ module XGen
class QueryMessage < Message class QueryMessage < Message
attr_reader :query
def initialize(db_name, collection_name, query) def initialize(db_name, collection_name, query)
super(OP_QUERY) super(OP_QUERY)
@query = query
write_int(0) write_int(0)
write_string("#{db_name}.#{collection_name}") write_string("#{db_name}.#{collection_name}")
write_int(query.number_to_skip) write_int(query.number_to_skip)

54
tests/test_cursor.rb Normal file
View File

@ -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