em-mongo 0.3.6 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/VERSION +1 -1
- data/lib/em-mongo/collection.rb +756 -23
- data/lib/em-mongo/connection.rb +100 -95
- data/lib/em-mongo/conversions.rb +2 -2
- data/lib/em-mongo/core_ext.rb +20 -0
- data/lib/em-mongo/cursor.rb +537 -0
- data/lib/em-mongo/database.rb +348 -20
- data/lib/em-mongo/exceptions.rb +2 -2
- data/lib/em-mongo/prev.rb +53 -0
- data/lib/em-mongo/request_response.rb +34 -0
- data/lib/em-mongo/server_response.rb +32 -0
- data/lib/em-mongo/support.rb +4 -4
- data/lib/em-mongo.rb +5 -0
- data/spec/integration/collection_spec.rb +654 -154
- data/spec/integration/cursor_spec.rb +350 -0
- data/spec/integration/database_spec.rb +149 -3
- data/spec/integration/request_response_spec.rb +63 -0
- metadata +12 -2
data/lib/em-mongo/connection.rb
CHANGED
@@ -5,6 +5,28 @@ module EM::Mongo
|
|
5
5
|
DEFAULT_NS = "ns"
|
6
6
|
DEFAULT_QUERY_DOCS = 101
|
7
7
|
|
8
|
+
OP_REPLY = 1
|
9
|
+
OP_MSG = 1000
|
10
|
+
OP_UPDATE = 2001
|
11
|
+
OP_INSERT = 2002
|
12
|
+
OP_QUERY = 2004
|
13
|
+
OP_GET_MORE = 2005
|
14
|
+
OP_DELETE = 2006
|
15
|
+
OP_KILL_CURSORS = 2007
|
16
|
+
|
17
|
+
OP_QUERY_TAILABLE = 2 ** 1
|
18
|
+
OP_QUERY_SLAVE_OK = 2 ** 2
|
19
|
+
OP_QUERY_OPLOG_REPLAY = 2 ** 3
|
20
|
+
OP_QUERY_NO_CURSOR_TIMEOUT = 2 ** 4
|
21
|
+
OP_QUERY_AWAIT_DATA = 2 ** 5
|
22
|
+
OP_QUERY_EXHAUST = 2 ** 6
|
23
|
+
|
24
|
+
ASCENDING = 1
|
25
|
+
DESCENDING = -1
|
26
|
+
GEO2D = '2d'
|
27
|
+
|
28
|
+
DEFAULT_MAX_BSON_SIZE = 4 * 1024 * 1024
|
29
|
+
|
8
30
|
class EMConnection < EM::Connection
|
9
31
|
MAX_RETRIES = 5
|
10
32
|
|
@@ -16,12 +38,6 @@ module EM::Mongo
|
|
16
38
|
include EM::Deferrable
|
17
39
|
|
18
40
|
RESERVED = 0
|
19
|
-
OP_REPLY = 1
|
20
|
-
OP_MSG = 1000
|
21
|
-
OP_UPDATE = 2001
|
22
|
-
OP_INSERT = 2002
|
23
|
-
OP_QUERY = 2004
|
24
|
-
OP_DELETE = 2006
|
25
41
|
|
26
42
|
STANDARD_HEADER_SIZE = 16
|
27
43
|
RESPONSE_HEADER_SIZE = 20
|
@@ -40,8 +56,35 @@ module EM::Mongo
|
|
40
56
|
@request_id += 1
|
41
57
|
end
|
42
58
|
|
59
|
+
def slave_ok?
|
60
|
+
@slave_ok
|
61
|
+
end
|
62
|
+
|
43
63
|
# MongoDB Commands
|
44
64
|
|
65
|
+
def prepare_message(op, message, options={})
|
66
|
+
req_id = new_request_id
|
67
|
+
message.prepend!(message_headers(op, req_id, message))
|
68
|
+
req_id = prepare_safe_message(message,options) if options[:safe]
|
69
|
+
[req_id, message.to_s]
|
70
|
+
end
|
71
|
+
|
72
|
+
def prepare_safe_message(message,options)
|
73
|
+
db_name = options[:db_name]
|
74
|
+
unless db_name
|
75
|
+
raise( ArgumentError, "You must include the :db_name option when :safe => true" )
|
76
|
+
end
|
77
|
+
|
78
|
+
last_error_params = options[:last_error_params] || false
|
79
|
+
last_error_message = BSON::ByteBuffer.new
|
80
|
+
|
81
|
+
build_last_error_message(last_error_message, db_name, last_error_params)
|
82
|
+
last_error_id = new_request_id
|
83
|
+
last_error_message.prepend!(message_headers(EM::Mongo::OP_QUERY, last_error_id, last_error_message))
|
84
|
+
message.append!(last_error_message)
|
85
|
+
last_error_id
|
86
|
+
end
|
87
|
+
|
45
88
|
def message_headers(operation, request_id, message)
|
46
89
|
headers = BSON::ByteBuffer.new
|
47
90
|
headers.put_int(16 + message.size)
|
@@ -51,75 +94,17 @@ module EM::Mongo
|
|
51
94
|
headers
|
52
95
|
end
|
53
96
|
|
54
|
-
def send_command(
|
97
|
+
def send_command(op, message, options={}, &cb)
|
98
|
+
request_id, buffer = prepare_message(op, message, options)
|
99
|
+
|
55
100
|
callback do
|
56
101
|
send_data buffer
|
57
102
|
end
|
58
103
|
|
59
|
-
@responses[request_id] =
|
104
|
+
@responses[request_id] = cb if cb
|
60
105
|
request_id
|
61
106
|
end
|
62
107
|
|
63
|
-
def insert(collection_name, documents)
|
64
|
-
message = BSON::ByteBuffer.new([0, 0, 0, 0])
|
65
|
-
BSON::BSON_RUBY.serialize_cstr(message, collection_name)
|
66
|
-
|
67
|
-
documents = [documents] if not documents.is_a?(Array)
|
68
|
-
documents.each { |doc| message.put_array(BSON::BSON_CODER.serialize(doc, true, true).to_a) }
|
69
|
-
|
70
|
-
req_id = new_request_id
|
71
|
-
message.prepend!(message_headers(OP_INSERT, req_id, message))
|
72
|
-
send_command(message.to_s, req_id)
|
73
|
-
end
|
74
|
-
|
75
|
-
def update(collection_name, selector, document, options)
|
76
|
-
message = BSON::ByteBuffer.new([0, 0, 0, 0])
|
77
|
-
BSON::BSON_RUBY.serialize_cstr(message, collection_name)
|
78
|
-
|
79
|
-
flags = 0
|
80
|
-
flags += 1 if options[:upsert]
|
81
|
-
flags += 2 if options[:multi]
|
82
|
-
message.put_int(flags)
|
83
|
-
|
84
|
-
message.put_array(BSON::BSON_CODER.serialize(selector, true, true).to_a)
|
85
|
-
message.put_array(BSON::BSON_CODER.serialize(document, false, true).to_a)
|
86
|
-
|
87
|
-
req_id = new_request_id
|
88
|
-
message.prepend!(message_headers(OP_UPDATE, req_id, message))
|
89
|
-
send_command(message.to_s, req_id)
|
90
|
-
end
|
91
|
-
|
92
|
-
def delete(collection_name, selector)
|
93
|
-
message = BSON::ByteBuffer.new([0, 0, 0, 0])
|
94
|
-
BSON::BSON_RUBY.serialize_cstr(message, collection_name)
|
95
|
-
message.put_int(0)
|
96
|
-
message.put_array(BSON::BSON_CODER.serialize(selector, false, true).to_a)
|
97
|
-
req_id = new_request_id
|
98
|
-
message.prepend!(message_headers(OP_DELETE, req_id, message))
|
99
|
-
send_command(message.to_s, req_id)
|
100
|
-
end
|
101
|
-
|
102
|
-
def find(collection_name, skip, limit, order, query, fields, &blk)
|
103
|
-
message = BSON::ByteBuffer.new
|
104
|
-
message.put_int(RESERVED) # query options
|
105
|
-
BSON::BSON_RUBY.serialize_cstr(message, collection_name)
|
106
|
-
message.put_int(skip)
|
107
|
-
message.put_int(limit)
|
108
|
-
query = order.nil? ? query : construct_query_spec(query, order)
|
109
|
-
message.put_array(BSON::BSON_CODER.serialize(query, false).to_a)
|
110
|
-
message.put_array(BSON::BSON_CODER.serialize(fields, false).to_a) if fields
|
111
|
-
req_id = new_request_id
|
112
|
-
message.prepend!(message_headers(OP_QUERY, req_id, message))
|
113
|
-
send_command(message.to_s, req_id, &blk)
|
114
|
-
end
|
115
|
-
|
116
|
-
def construct_query_spec(query, order)
|
117
|
-
spec = BSON::OrderedHash.new
|
118
|
-
spec['$query'] = query
|
119
|
-
spec['$orderby'] = Mongo::Support.format_order_clause(order) if order
|
120
|
-
spec
|
121
|
-
end
|
122
|
-
|
123
108
|
# EM hooks
|
124
109
|
def initialize(options={})
|
125
110
|
@request_id = 0
|
@@ -130,6 +115,7 @@ module EM::Mongo
|
|
130
115
|
@port = options[:port] || DEFAULT_PORT
|
131
116
|
@on_unbind = options[:unbind_cb] || proc {}
|
132
117
|
@reconnect_in = options[:reconnect_in]|| false
|
118
|
+
@slave_ok = options[:slave_ok] || false
|
133
119
|
|
134
120
|
@on_close = proc {
|
135
121
|
raise Error, "failure with mongodb server #{@host}:#{@port}"
|
@@ -172,9 +158,9 @@ module EM::Mongo
|
|
172
158
|
|
173
159
|
@buffer.rewind
|
174
160
|
while message_received?(@buffer)
|
175
|
-
|
176
|
-
callback = @responses.delete(response_to)
|
177
|
-
callback.call(
|
161
|
+
response = next_response
|
162
|
+
callback = @responses.delete(response.response_to)
|
163
|
+
callback.call(response) if callback
|
178
164
|
end
|
179
165
|
|
180
166
|
if @buffer.more?
|
@@ -190,33 +176,12 @@ module EM::Mongo
|
|
190
176
|
end
|
191
177
|
|
192
178
|
def next_response()
|
193
|
-
|
194
|
-
# Header
|
195
|
-
size = @buffer.get_int
|
196
|
-
request_id = @buffer.get_int
|
197
|
-
response_to = @buffer.get_int
|
198
|
-
op = @buffer.get_int
|
199
|
-
#puts "message header #{size} #{request_id} #{response_to} #{op}"
|
200
|
-
|
201
|
-
# Response Header
|
202
|
-
result_flags = @buffer.get_int
|
203
|
-
cursor_id = @buffer.get_long
|
204
|
-
starting_from = @buffer.get_int
|
205
|
-
number_returned = @buffer.get_int
|
206
|
-
#puts "response header #{result_flags} #{cursor_id} #{starting_from} #{number_returned}"
|
207
|
-
|
208
|
-
# Documents
|
209
|
-
docs = (1..number_returned).map do
|
210
|
-
size= peek_size(@buffer)
|
211
|
-
buf = @buffer.get(size)
|
212
|
-
BSON::BSON_CODER.deserialize(buf)
|
213
|
-
end
|
214
|
-
[response_to,docs]
|
179
|
+
ServerResponse.new(@buffer, self)
|
215
180
|
end
|
216
181
|
|
217
182
|
def unbind
|
218
183
|
if @is_connected
|
219
|
-
@responses.values.each { |
|
184
|
+
@responses.values.each { |resp| resp.call(:disconnected) }
|
220
185
|
|
221
186
|
@request_id = 0
|
222
187
|
@responses = {}
|
@@ -246,26 +211,66 @@ module EM::Mongo
|
|
246
211
|
end
|
247
212
|
end
|
248
213
|
|
214
|
+
# Constructs a getlasterror message. This method is used exclusively by
|
215
|
+
# Connection#send_message_with_safe_check.
|
216
|
+
#
|
217
|
+
# Because it modifies message by reference, we don't need to return it.
|
218
|
+
def build_last_error_message(message, db_name, opts)
|
219
|
+
message.put_int(0)
|
220
|
+
BSON::BSON_RUBY.serialize_cstr(message, "#{db_name}.$cmd")
|
221
|
+
message.put_int(0)
|
222
|
+
message.put_int(-1)
|
223
|
+
cmd = BSON::OrderedHash.new
|
224
|
+
cmd[:getlasterror] = 1
|
225
|
+
if opts.is_a?(Hash)
|
226
|
+
opts.assert_valid_keys(:w, :wtimeout, :fsync)
|
227
|
+
cmd.merge!(opts)
|
228
|
+
end
|
229
|
+
message.put_binary(BSON::BSON_CODER.serialize(cmd, false).to_s)
|
230
|
+
nil
|
231
|
+
end
|
232
|
+
|
249
233
|
end
|
250
234
|
|
235
|
+
# An em-mongo Connection
|
251
236
|
class Connection
|
252
237
|
|
238
|
+
# Initialize and connect to a MongoDB instance
|
239
|
+
# @param [String] host the host name or IP of the mongodb server to connect to
|
240
|
+
# @param [Integer] port the port the mongodb server is listening on
|
241
|
+
# @param [Integer] timeout the connection timeout
|
242
|
+
# @opts [Hash] opts connection options
|
253
243
|
def initialize(host = DEFAULT_IP, port = DEFAULT_PORT, timeout = nil, opts = {})
|
254
244
|
@em_connection = EMConnection.connect(host, port, timeout, opts)
|
255
245
|
@db = {}
|
256
246
|
end
|
257
247
|
|
248
|
+
# Return a database with the given name.
|
249
|
+
#
|
250
|
+
# @param [String] db_name a valid database name.
|
251
|
+
#
|
252
|
+
# @return [EM::Mongo::Database]
|
258
253
|
def db(name = DEFAULT_DB)
|
259
|
-
@db[name] ||= EM::Mongo::Database.new(name,
|
254
|
+
@db[name] ||= EM::Mongo::Database.new(name, self)
|
260
255
|
end
|
261
256
|
|
257
|
+
# Close the connection to the database.
|
262
258
|
def close
|
263
259
|
@em_connection.close
|
264
260
|
end
|
265
261
|
|
262
|
+
#@return [true, false]
|
263
|
+
# whether or not the connection is currently connected
|
266
264
|
def connected?
|
267
265
|
@em_connection.connected?
|
268
266
|
end
|
267
|
+
|
268
|
+
def send_command(*args, &block);@em_connection.send_command(*args, &block);end
|
269
|
+
|
270
|
+
# Is it okay to connect to a slave?
|
271
|
+
#
|
272
|
+
# @return [Boolean]
|
273
|
+
def slave_ok?;@em_connection.slave_ok?;end
|
269
274
|
|
270
275
|
end
|
271
276
|
end
|
data/lib/em-mongo/conversions.rb
CHANGED
@@ -15,7 +15,7 @@
|
|
15
15
|
# See the License for the specific language governing permissions and
|
16
16
|
# limitations under the License.
|
17
17
|
# ++
|
18
|
-
module Mongo #:nodoc:
|
18
|
+
module EM::Mongo #:nodoc:
|
19
19
|
|
20
20
|
# Utility module to include when needing to convert certain types of
|
21
21
|
# objects to mongo-friendly parameters.
|
@@ -82,7 +82,7 @@ module Mongo #:nodoc:
|
|
82
82
|
return -1 if DESCENDING_CONVERSION.include?(val)
|
83
83
|
raise InvalidSortValueError.new(
|
84
84
|
"#{self} was supplied as a sort direction when acceptable values are: " +
|
85
|
-
"Mongo::ASCENDING, 'ascending', 'asc', :ascending, :asc, 1, Mongo::DESCENDING, " +
|
85
|
+
"EM::Mongo::ASCENDING, 'ascending', 'asc', :ascending, :asc, 1, EM::Mongo::DESCENDING, " +
|
86
86
|
"'descending', 'desc', :descending, :desc, -1.")
|
87
87
|
end
|
88
88
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
#:nodoc:
|
2
|
+
class String
|
3
|
+
|
4
|
+
#:nodoc:
|
5
|
+
def to_bson_code
|
6
|
+
BSON::Code.new(self)
|
7
|
+
end
|
8
|
+
|
9
|
+
end
|
10
|
+
|
11
|
+
#:nodoc:
|
12
|
+
class Hash
|
13
|
+
|
14
|
+
#:nodoc:
|
15
|
+
def assert_valid_keys(*valid_keys)
|
16
|
+
unknown_keys = keys - [valid_keys].flatten
|
17
|
+
raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
@@ -0,0 +1,537 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
# Copyright (C) 2008-2011 10gen Inc.
|
4
|
+
#
|
5
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
6
|
+
# you may not use this file except in compliance with the License.
|
7
|
+
# You may obtain a copy of the License at
|
8
|
+
#
|
9
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
10
|
+
#
|
11
|
+
# Unless required by applicable law or agreed to in writing, software
|
12
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
13
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
14
|
+
# See the License for the specific language governing permissions and
|
15
|
+
# limitations under the License.
|
16
|
+
|
17
|
+
module EM::Mongo
|
18
|
+
|
19
|
+
# A cursor over query results. Returned objects are hashes.
|
20
|
+
class Cursor
|
21
|
+
include EM::Mongo::Conversions
|
22
|
+
#include Enumerable
|
23
|
+
|
24
|
+
attr_reader :collection, :selector, :fields,
|
25
|
+
:order, :hint, :snapshot, :timeout,
|
26
|
+
:full_collection_name, :transformer
|
27
|
+
|
28
|
+
# Create a new cursor.
|
29
|
+
#
|
30
|
+
# Note: cursors are created when executing queries using [Collection#find] and other
|
31
|
+
# similar methods. Application developers shouldn't have to create cursors manually.
|
32
|
+
#
|
33
|
+
# @return [Cursor]
|
34
|
+
#
|
35
|
+
# @core cursors constructor_details
|
36
|
+
def initialize(collection, opts={})
|
37
|
+
@cursor_id = nil
|
38
|
+
|
39
|
+
@db = collection.db
|
40
|
+
@collection = collection
|
41
|
+
@connection = @db.connection
|
42
|
+
#@logger = @connection.logger
|
43
|
+
|
44
|
+
# Query selector
|
45
|
+
@selector = opts[:selector] || {}
|
46
|
+
|
47
|
+
# Special operators that form part of $query
|
48
|
+
@order = opts[:order]
|
49
|
+
@explain = opts[:explain]
|
50
|
+
@hint = opts[:hint]
|
51
|
+
@snapshot = opts[:snapshot]
|
52
|
+
@max_scan = opts.fetch(:max_scan, nil)
|
53
|
+
@return_key = opts.fetch(:return_key, nil)
|
54
|
+
@show_disk_loc = opts.fetch(:show_disk_loc, nil)
|
55
|
+
|
56
|
+
# Wire-protocol settings
|
57
|
+
@fields = convert_fields_for_query(opts[:fields])
|
58
|
+
@skip = opts[:skip] || 0
|
59
|
+
@limit = opts[:limit] || 0
|
60
|
+
@tailable = opts[:tailable] || false
|
61
|
+
@timeout = opts.fetch(:timeout, true)
|
62
|
+
|
63
|
+
# Use this socket for the query
|
64
|
+
#@socket = opts[:socket]
|
65
|
+
|
66
|
+
@closed = false
|
67
|
+
@query_run = false
|
68
|
+
|
69
|
+
@transformer = opts[:transformer]
|
70
|
+
batch_size(opts[:batch_size] || 0)
|
71
|
+
|
72
|
+
@full_collection_name = "#{@collection.db.name}.#{@collection.name}"
|
73
|
+
@cache = []
|
74
|
+
@returned = 0
|
75
|
+
|
76
|
+
if @collection.name =~ /^\$cmd/ || @collection.name =~ /^system/
|
77
|
+
@command = true
|
78
|
+
else
|
79
|
+
@command = false
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Get the next document specified the cursor options.
|
84
|
+
#
|
85
|
+
# @return [EM::Mongo::RequestResponse] Calls back with the next document or Nil if no documents remain.
|
86
|
+
def next_document
|
87
|
+
response = RequestResponse.new
|
88
|
+
if @cache.length == 0
|
89
|
+
refresh.callback do
|
90
|
+
check_and_transform_document(@cache.shift, response)
|
91
|
+
end
|
92
|
+
else
|
93
|
+
check_and_transform_document(@cache.shift, response)
|
94
|
+
end
|
95
|
+
response
|
96
|
+
end
|
97
|
+
alias :next :next_document
|
98
|
+
|
99
|
+
def check_and_transform_document(doc, response)
|
100
|
+
return response.succeed(nil) if doc.nil?
|
101
|
+
|
102
|
+
if doc['$err']
|
103
|
+
|
104
|
+
err = doc['$err']
|
105
|
+
|
106
|
+
# If the server has stopped being the master (e.g., it's one of a
|
107
|
+
# pair but it has died or something like that) then we close that
|
108
|
+
# connection. The next request will re-open on master server.
|
109
|
+
if err == "not master"
|
110
|
+
@connection.close
|
111
|
+
response.fail([ConnectionFailure, err])
|
112
|
+
else
|
113
|
+
response.fail([OperationFailure, err])
|
114
|
+
end
|
115
|
+
|
116
|
+
else
|
117
|
+
response.succeed(
|
118
|
+
@transformer ? @transformer.call(doc) : doc
|
119
|
+
)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
private :check_and_transform_document
|
123
|
+
|
124
|
+
# Reset this cursor on the server. Cursor options, such as the
|
125
|
+
# query string and the values for skip and limit, are preserved.
|
126
|
+
def rewind!
|
127
|
+
close
|
128
|
+
@cache.clear
|
129
|
+
@cursor_id = nil
|
130
|
+
@closed = false
|
131
|
+
@query_run = false
|
132
|
+
@n_received = nil
|
133
|
+
end
|
134
|
+
|
135
|
+
# Determine whether this cursor has any remaining results.
|
136
|
+
#
|
137
|
+
# @return [EM::Mongo::RequestResponse]
|
138
|
+
def has_next?
|
139
|
+
response = RequestResponse.new
|
140
|
+
num_resp = num_remaining
|
141
|
+
num_resp.callback { |num| response.succeed( num > 0 ) }
|
142
|
+
num_resp.errback { |err| response.fail err }
|
143
|
+
response
|
144
|
+
end
|
145
|
+
|
146
|
+
# Get the size of the result set for this query.
|
147
|
+
#
|
148
|
+
# @param [Boolean] whether of not to take notice of skip and limit
|
149
|
+
#
|
150
|
+
# @return [EM::Mongo::RequestResponse] Calls back with the number of objects in the result set for this query.
|
151
|
+
#
|
152
|
+
# @raise [OperationFailure] on a database error.
|
153
|
+
def count(skip_and_limit = false)
|
154
|
+
response = RequestResponse.new
|
155
|
+
command = BSON::OrderedHash["count", @collection.name, "query", @selector]
|
156
|
+
|
157
|
+
if skip_and_limit
|
158
|
+
command.merge!(BSON::OrderedHash["limit", @limit]) if @limit != 0
|
159
|
+
command.merge!(BSON::OrderedHash["skip", @skip]) if @skip != 0
|
160
|
+
end
|
161
|
+
|
162
|
+
command.merge!(BSON::OrderedHash["fields", @fields])
|
163
|
+
|
164
|
+
cmd_resp = @db.command(command)
|
165
|
+
|
166
|
+
cmd_resp.callback { |doc| response.succeed( doc['n'].to_i ) }
|
167
|
+
cmd_resp.errback do |err|
|
168
|
+
if err[1] =~ /ns missing/
|
169
|
+
response.succeed(0)
|
170
|
+
else
|
171
|
+
response.fail([OperationFailure, "Count failed: #{err[1]}"])
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
response
|
176
|
+
end
|
177
|
+
|
178
|
+
# Sort this cursor's results.
|
179
|
+
#
|
180
|
+
# This method overrides any sort order specified in the Collection#find
|
181
|
+
# method, and only the last sort applied has an effect.
|
182
|
+
#
|
183
|
+
# @param [Symbol, Array] key_or_list either 1) a key to sort by or 2)
|
184
|
+
# an array of [key, direction] pairs to sort by. Direction should
|
185
|
+
# be specified as EM::Mongo::ASCENDING (or :ascending / :asc) or EM::Mongo::DESCENDING (or :descending / :desc)
|
186
|
+
#
|
187
|
+
# @raise [InvalidOperation] if this cursor has already been used.
|
188
|
+
#
|
189
|
+
# @raise [InvalidSortValueError] if the specified order is invalid.
|
190
|
+
def sort(key_or_list, direction=nil)
|
191
|
+
check_modifiable
|
192
|
+
|
193
|
+
if !direction.nil?
|
194
|
+
order = [[key_or_list, direction]]
|
195
|
+
else
|
196
|
+
order = key_or_list
|
197
|
+
end
|
198
|
+
|
199
|
+
@order = order
|
200
|
+
self
|
201
|
+
end
|
202
|
+
|
203
|
+
# Limit the number of results to be returned by this cursor.
|
204
|
+
#
|
205
|
+
# This method overrides any limit specified in the Collection#find method,
|
206
|
+
# and only the last limit applied has an effect.
|
207
|
+
#
|
208
|
+
# @return [Integer] the current number_to_return if no parameter is given.
|
209
|
+
#
|
210
|
+
# @raise [InvalidOperation] if this cursor has already been used.
|
211
|
+
#
|
212
|
+
# @core limit limit-instance_method
|
213
|
+
def limit(number_to_return=nil)
|
214
|
+
return @limit unless number_to_return
|
215
|
+
check_modifiable
|
216
|
+
|
217
|
+
@limit = number_to_return
|
218
|
+
self
|
219
|
+
end
|
220
|
+
|
221
|
+
# Skips the first +number_to_skip+ results of this cursor.
|
222
|
+
# Returns the current number_to_skip if no parameter is given.
|
223
|
+
#
|
224
|
+
# This method overrides any skip specified in the Collection#find method,
|
225
|
+
# and only the last skip applied has an effect.
|
226
|
+
#
|
227
|
+
# @return [Integer]
|
228
|
+
#
|
229
|
+
# @raise [InvalidOperation] if this cursor has already been used.
|
230
|
+
def skip(number_to_skip=nil)
|
231
|
+
return @skip unless number_to_skip
|
232
|
+
check_modifiable
|
233
|
+
|
234
|
+
@skip = number_to_skip
|
235
|
+
self
|
236
|
+
end
|
237
|
+
|
238
|
+
# Set the batch size for server responses.
|
239
|
+
#
|
240
|
+
# Note that the batch size will take effect only on queries
|
241
|
+
# where the number to be returned is greater than 100.
|
242
|
+
#
|
243
|
+
# @param [Integer] size either 0 or some integer greater than 1. If 0,
|
244
|
+
# the server will determine the batch size.
|
245
|
+
#
|
246
|
+
# @return [Cursor]
|
247
|
+
def batch_size(size=0)
|
248
|
+
check_modifiable
|
249
|
+
if size < 0 || size == 1
|
250
|
+
raise ArgumentError, "Invalid value for batch_size #{size}; must be 0 or > 1."
|
251
|
+
else
|
252
|
+
@batch_size = size > @limit ? @limit : size
|
253
|
+
end
|
254
|
+
|
255
|
+
self
|
256
|
+
end
|
257
|
+
|
258
|
+
# Iterate over each document in this cursor, yielding it to the given
|
259
|
+
# block.
|
260
|
+
#
|
261
|
+
# Iterating over an entire cursor will close it.
|
262
|
+
#
|
263
|
+
# @yield passes each document to a block for processing. When the cursor is empty,
|
264
|
+
# each will yield a nil value
|
265
|
+
#
|
266
|
+
# @example if 'comments' represents a collection of comments:
|
267
|
+
# comments.find.each do |doc|
|
268
|
+
# if doc
|
269
|
+
# puts doc['user']
|
270
|
+
# end
|
271
|
+
# end
|
272
|
+
def each(&blk)
|
273
|
+
raise "A callback block is required for #each" unless blk
|
274
|
+
EM.next_tick do
|
275
|
+
next_doc_resp = next_document
|
276
|
+
next_doc_resp.callback do |doc|
|
277
|
+
blk.call(doc)
|
278
|
+
doc.nil? ? close : self.each(&blk)
|
279
|
+
end
|
280
|
+
next_doc_resp.errback do |err|
|
281
|
+
if blk.arity > 1
|
282
|
+
blk.call(:error, err)
|
283
|
+
else
|
284
|
+
blk.call(:error)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Receive all the documents from this cursor as an array of hashes.
|
291
|
+
#
|
292
|
+
# Notes:
|
293
|
+
#
|
294
|
+
# If you've already started iterating over the cursor, the array returned
|
295
|
+
# by this method contains only the remaining documents. See Cursor#rewind! if you
|
296
|
+
# need to reset the cursor.
|
297
|
+
#
|
298
|
+
# Use of this method is discouraged - in most cases, it's much more
|
299
|
+
# efficient to retrieve documents as you need them by iterating over the cursor.
|
300
|
+
#
|
301
|
+
# @return [EM::Mongo::RequestResponse] Calls back with an array of documents.
|
302
|
+
def to_a
|
303
|
+
response = RequestResponse.new
|
304
|
+
items = []
|
305
|
+
self.each do |doc,err|
|
306
|
+
if doc == :error
|
307
|
+
response.fail(err)
|
308
|
+
elsif doc
|
309
|
+
items << doc
|
310
|
+
else
|
311
|
+
response.succeed(items)
|
312
|
+
end
|
313
|
+
end
|
314
|
+
response
|
315
|
+
end
|
316
|
+
|
317
|
+
# Get the explain plan for this cursor.
|
318
|
+
#
|
319
|
+
# @return [EM::Mongo::RequestResponse] Calls back with a document containing the explain plan for this cursor.
|
320
|
+
#
|
321
|
+
# @core explain explain-instance_method
|
322
|
+
def explain
|
323
|
+
response = RequestResponse.new
|
324
|
+
c = Cursor.new(@collection, query_options_hash.merge(:limit => -@limit.abs, :explain => true))
|
325
|
+
|
326
|
+
exp_response = c.next_document
|
327
|
+
exp_response.callback do |explanation|
|
328
|
+
c.close
|
329
|
+
response.succeed(explanation)
|
330
|
+
end
|
331
|
+
exp_response.errback do |err|
|
332
|
+
c.close
|
333
|
+
response.fail(err)
|
334
|
+
end
|
335
|
+
response
|
336
|
+
end
|
337
|
+
|
338
|
+
# Close the cursor.
|
339
|
+
#
|
340
|
+
# Note: if a cursor is read until exhausted (read until EM::Mongo::Constants::OP_QUERY or
|
341
|
+
# EM::Mongo::Constants::OP_GETMORE returns zero for the cursor id), there is no need to
|
342
|
+
# close it manually.
|
343
|
+
#
|
344
|
+
# Note also: Collection#find takes an optional block argument which can be used to
|
345
|
+
# ensure that your cursors get closed.
|
346
|
+
#
|
347
|
+
# @return [True]
|
348
|
+
def close
|
349
|
+
if @cursor_id && @cursor_id != 0
|
350
|
+
@cursor_id = 0
|
351
|
+
@closed = true
|
352
|
+
message = BSON::ByteBuffer.new([0, 0, 0, 0])
|
353
|
+
message.put_int(1)
|
354
|
+
message.put_long(@cursor_id)
|
355
|
+
@connection.send_command(EM::Mongo::OP_KILL_CURSORS, message)
|
356
|
+
end
|
357
|
+
true
|
358
|
+
end
|
359
|
+
|
360
|
+
# Is this cursor closed?
|
361
|
+
#
|
362
|
+
# @return [Boolean]
|
363
|
+
def closed?; @closed; end
|
364
|
+
|
365
|
+
# Returns an integer indicating which query options have been selected.
|
366
|
+
#
|
367
|
+
# @return [Integer]
|
368
|
+
#
|
369
|
+
# @see http://www.mongodb.org/display/DOCS/Mongo+Wire+Protocol#MongoWireProtocol-EM::Mongo::Constants::OPQUERY
|
370
|
+
# The MongoDB wire protocol.
|
371
|
+
def query_opts
|
372
|
+
opts = 0
|
373
|
+
opts |= EM::Mongo::OP_QUERY_NO_CURSOR_TIMEOUT unless @timeout
|
374
|
+
opts |= EM::Mongo::OP_QUERY_SLAVE_OK if @connection.slave_ok?
|
375
|
+
opts |= EM::Mongo::OP_QUERY_TAILABLE if @tailable
|
376
|
+
opts
|
377
|
+
end
|
378
|
+
|
379
|
+
# Get the query options for this Cursor.
|
380
|
+
#
|
381
|
+
# @return [Hash]
|
382
|
+
def query_options_hash
|
383
|
+
{ :selector => @selector,
|
384
|
+
:fields => @fields,
|
385
|
+
:skip => @skip,
|
386
|
+
:limit => @limit,
|
387
|
+
:order => @order,
|
388
|
+
:hint => @hint,
|
389
|
+
:snapshot => @snapshot,
|
390
|
+
:timeout => @timeout,
|
391
|
+
:max_scan => @max_scan,
|
392
|
+
:return_key => @return_key,
|
393
|
+
:show_disk_loc => @show_disk_loc }
|
394
|
+
end
|
395
|
+
|
396
|
+
# Clean output for inspect.
|
397
|
+
def inspect
|
398
|
+
"<EM::Mongo::Cursor:0x#{object_id.to_s} namespace='#{@db.name}.#{@collection.name}' " +
|
399
|
+
"@selector=#{@selector.inspect}>"
|
400
|
+
end
|
401
|
+
|
402
|
+
private
|
403
|
+
|
404
|
+
# Convert the +:fields+ parameter from a single field name or an array
|
405
|
+
# of fields names to a hash, with the field names for keys and '1' for each
|
406
|
+
# value.
|
407
|
+
def convert_fields_for_query(fields)
|
408
|
+
case fields
|
409
|
+
when String, Symbol
|
410
|
+
{fields => 1}
|
411
|
+
when Array
|
412
|
+
return nil if fields.length.zero?
|
413
|
+
fields.each_with_object({}) { |field, hash| hash[field] = 1 }
|
414
|
+
when Hash
|
415
|
+
return fields
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
# Return the number of documents remaining for this cursor.
|
420
|
+
# @return [EM::Mongo::RequestResponse]
|
421
|
+
def num_remaining
|
422
|
+
response = RequestResponse.new
|
423
|
+
if @cache.length == 0
|
424
|
+
ref_resp = refresh
|
425
|
+
ref_resp.callback { response.succeed(@cache.length) }
|
426
|
+
ref_resp.errback { |err| response.fail err }
|
427
|
+
else
|
428
|
+
response.succeed(@cache.length)
|
429
|
+
end
|
430
|
+
response
|
431
|
+
end
|
432
|
+
|
433
|
+
def refresh
|
434
|
+
return RequestResponse.new.tap{|d|d.succeed} if @cursor_id && @cursor_id.zero?
|
435
|
+
return send_initial_query unless @query_run
|
436
|
+
|
437
|
+
message = BSON::ByteBuffer.new([0, 0, 0, 0])
|
438
|
+
|
439
|
+
# DB name.
|
440
|
+
BSON::BSON_RUBY.serialize_cstr(message, "#{@db.name}.#{@collection.name}")
|
441
|
+
|
442
|
+
# Number of results to return.
|
443
|
+
if @limit > 0
|
444
|
+
limit = @limit - @returned
|
445
|
+
if @batch_size > 0
|
446
|
+
limit = limit < @batch_size ? limit : @batch_size
|
447
|
+
end
|
448
|
+
message.put_int(limit)
|
449
|
+
else
|
450
|
+
message.put_int(@batch_size)
|
451
|
+
end
|
452
|
+
|
453
|
+
# Cursor id.
|
454
|
+
message.put_long(@cursor_id)
|
455
|
+
|
456
|
+
response = RequestResponse.new
|
457
|
+
@connection.send_command(EM::Mongo::OP_GET_MORE, message) do |resp|
|
458
|
+
if resp == :disconnected
|
459
|
+
response.fail(:disconnected)
|
460
|
+
else
|
461
|
+
@cache += resp.docs
|
462
|
+
@n_received = resp.number_returned
|
463
|
+
@returned += @n_received
|
464
|
+
close_cursor_if_query_complete
|
465
|
+
response.succeed
|
466
|
+
end
|
467
|
+
end
|
468
|
+
response
|
469
|
+
end
|
470
|
+
|
471
|
+
# Run query the first time we request an object from the wire
|
472
|
+
def send_initial_query
|
473
|
+
response = RequestResponse.new
|
474
|
+
message = construct_query_message
|
475
|
+
@connection.send_command(EM::Mongo::OP_QUERY, message) do |resp|
|
476
|
+
if resp == :disconnected
|
477
|
+
response.fail(:disconnected)
|
478
|
+
else
|
479
|
+
@cache += resp.docs
|
480
|
+
@n_received = resp.number_returned
|
481
|
+
@cursor_id = resp.cursor_id
|
482
|
+
@returned += @n_received
|
483
|
+
@query_run = true
|
484
|
+
close_cursor_if_query_complete
|
485
|
+
response.succeed
|
486
|
+
end
|
487
|
+
end
|
488
|
+
response
|
489
|
+
end
|
490
|
+
|
491
|
+
def construct_query_message
|
492
|
+
message = BSON::ByteBuffer.new
|
493
|
+
message.put_int(query_opts)
|
494
|
+
BSON::BSON_RUBY.serialize_cstr(message, "#{@db.name}.#{@collection.name}")
|
495
|
+
message.put_int(@skip)
|
496
|
+
message.put_int(@limit)
|
497
|
+
spec = query_contains_special_fields? ? construct_query_spec : @selector
|
498
|
+
message.put_binary(BSON::BSON_CODER.serialize(spec, false).to_s)
|
499
|
+
message.put_binary(BSON::BSON_CODER.serialize(@fields, false).to_s) if @fields
|
500
|
+
message
|
501
|
+
end
|
502
|
+
|
503
|
+
|
504
|
+
def construct_query_spec
|
505
|
+
return @selector if @selector.has_key?('$query')
|
506
|
+
spec = BSON::OrderedHash.new
|
507
|
+
spec['$query'] = @selector
|
508
|
+
spec['$orderby'] = EM::Mongo::Support.format_order_clause(@order) if @order
|
509
|
+
spec['$hint'] = @hint if @hint && @hint.length > 0
|
510
|
+
spec['$explain'] = true if @explain
|
511
|
+
spec['$snapshot'] = true if @snapshot
|
512
|
+
spec['$maxscan'] = @max_scan if @max_scan
|
513
|
+
spec['$returnKey'] = true if @return_key
|
514
|
+
spec['$showDiskLoc'] = true if @show_disk_loc
|
515
|
+
spec
|
516
|
+
end
|
517
|
+
|
518
|
+
# Returns true if the query contains order, explain, hint, or snapshot.
|
519
|
+
def query_contains_special_fields?
|
520
|
+
@order || @explain || @hint || @snapshot
|
521
|
+
end
|
522
|
+
|
523
|
+
def to_s
|
524
|
+
"DBResponse(flags=#@result_flags, cursor_id=#@cursor_id, start=#@starting_from)"
|
525
|
+
end
|
526
|
+
|
527
|
+
def close_cursor_if_query_complete
|
528
|
+
close if @limit > 0 && @returned >= @limit
|
529
|
+
end
|
530
|
+
|
531
|
+
def check_modifiable
|
532
|
+
if @query_run || @closed
|
533
|
+
raise InvalidOperation, "Cannot modify the query once it has been run or closed."
|
534
|
+
end
|
535
|
+
end
|
536
|
+
end
|
537
|
+
end
|