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