em-mongo 0.3.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|