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.
@@ -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(buffer, request_id, &blk)
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] = blk if blk
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
- response_to, docs= next_response
176
- callback = @responses.delete(response_to)
177
- callback.call(docs) if callback
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 { |blk| blk.call(:disconnected) }
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, @em_connection)
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
@@ -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