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 CHANGED
@@ -1 +1 @@
1
- 0.3.6
1
+ 0.4.0
@@ -2,6 +2,13 @@ module EM::Mongo
2
2
  class Collection
3
3
  attr_accessor :connection
4
4
 
5
+ # Initialize a collection object.
6
+ #
7
+ # @param [String, Symbol] db the name of the database to which this collection belongs.
8
+ # @param [String, Symbol] ns the name of the collection
9
+ # @param [Connection] connection the EM::Mongo::Connection that will service this collection
10
+ #
11
+ # @return [Collection]
5
12
  def initialize(db, ns, connection = nil)
6
13
  @db = db || "db"
7
14
  @ns = ns || "ns"
@@ -9,51 +16,680 @@ module EM::Mongo
9
16
  @connection = connection || EM::Mongo::Connection.new
10
17
  end
11
18
 
12
- def find(selector={}, opts={}, &blk)
13
- raise "find requires a block" if not block_given?
19
+ # The database that this collection belongs to
20
+ # @return [EM::Mongo::Database]
21
+ def db
22
+ connection.db(@db)
23
+ end
14
24
 
15
- skip = opts.delete(:skip) || 0
16
- limit = opts.delete(:limit) || 0
17
- order = opts.delete(:order)
25
+ #The name of this collection
26
+ # @return [String]
27
+ def name
28
+ @ns
29
+ end
18
30
 
19
- @connection.find(@name, skip, limit, order, selector, nil, &blk)
31
+ # Return a sub-collection of this collection by name. If 'users' is a collection, then
32
+ # 'users.comments' is a sub-collection of users.
33
+ #
34
+ # @param [String, Symbol] name
35
+ # the collection to return
36
+ #
37
+ # @return [Collection]
38
+ # the specified sub-collection
39
+ def [](name)
40
+ name = "#{self.name}.#{name}"
41
+ db.collection(name)
20
42
  end
21
43
 
22
- def first(selector={}, opts={}, &blk)
23
- opts[:limit] = 1
24
- find(selector, opts) do |res|
25
- yield res.first
44
+ # Query the database.
45
+ #
46
+ # The +selector+ argument is a prototype document that all results must
47
+ # match. For example:
48
+ #
49
+ # collection.find({"hello" => "world"})
50
+ #
51
+ # only matches documents that have a key "hello" with value "world".
52
+ # Matches can have other keys *in addition* to "hello".
53
+ #
54
+ # @return [EM::Mongo::Cursor]
55
+ # a cursor over the results of the query
56
+ #
57
+ # @param [Hash] selector
58
+ # a document specifying elements which must be present for a
59
+ # document to be included in the result set. Note that in rare cases,
60
+ # (e.g., with $near queries), the order of keys will matter. To preserve
61
+ # key order on a selector, use an instance of BSON::OrderedHash (only applies
62
+ # to Ruby 1.8).
63
+ #
64
+ # @option opts [Array, Hash] :fields field names that should be returned in the result
65
+ # set ("_id" will be included unless explicity excluded). By limiting results to a certain subset of fields,
66
+ # you can cut down on network traffic and decoding time. If using a Hash, keys should be field
67
+ # names and values should be either 1 or 0, depending on whether you want to include or exclude
68
+ # the given field.
69
+ # @option opts [Integer] :skip number of documents to skip from the beginning of the result set
70
+ # @option opts [Integer] :limit maximum number of documents to return
71
+ # @option opts [Array] :sort an array of [key, direction] pairs to sort by. Direction should
72
+ # be specified as Mongo::ASCENDING (or :ascending / :asc) or Mongo::DESCENDING (or :descending / :desc)
73
+ # @option opts [String, Array, OrderedHash] :hint hint for query optimizer, usually not necessary if
74
+ # using MongoDB > 1.1
75
+ # @option opts [Boolean] :snapshot (false) if true, snapshot mode will be used for this query.
76
+ # Snapshot mode assures no duplicates are returned, or objects missed, which were preset at both the start and
77
+ # end of the query's execution.
78
+ # For details see http://www.mongodb.org/display/DOCS/How+to+do+Snapshotting+in+the+Mongo+Database
79
+ # @option opts [Boolean] :batch_size (100) the number of documents to returned by the database per
80
+ # GETMORE operation. A value of 0 will let the database server decide how many results to returns.
81
+ # This option can be ignored for most use cases.
82
+ # @option opts [Boolean] :timeout (true) when +true+, the returned cursor will be subject to
83
+ # the normal cursor timeout behavior of the mongod process. Disabling the timeout is not supported by em-mongo
84
+ # @option opts [Integer] :max_scan (nil) Limit the number of items to scan on both collection scans and indexed queries..
85
+ # @option opts [Boolean] :show_disk_loc (false) Return the disk location of each query result (for debugging).
86
+ # @option opts [Boolean] :return_key (false) Return the index key used to obtain the result (for debugging).
87
+ # @option opts [Block] :transformer (nil) a block for tranforming returned documents.
88
+ # This is normally used by object mappers to convert each returned document to an instance of a class.
89
+ #
90
+ # @raise [ArgumentError]
91
+ # if timeout is set to false
92
+ #
93
+ # @raise [RuntimeError]
94
+ # if given unknown options
95
+ #
96
+ # @core find find-instance_method
97
+ def find(selector={}, opts={})
98
+ opts = opts.dup
99
+ fields = opts.delete(:fields)
100
+ fields = ["_id"] if fields && fields.empty?
101
+ skip = opts.delete(:skip) || skip || 0
102
+ limit = opts.delete(:limit) || 0
103
+ sort = opts.delete(:sort) || opts.delete(:order)
104
+ hint = opts.delete(:hint)
105
+ snapshot = opts.delete(:snapshot)
106
+ batch_size = opts.delete(:batch_size)
107
+ timeout = (opts.delete(:timeout) == false) ? false : true
108
+ max_scan = opts.delete(:max_scan)
109
+ return_key = opts.delete(:return_key)
110
+ transformer = opts.delete(:transformer)
111
+ show_disk_loc = opts.delete(:max_scan)
112
+
113
+ if timeout == false
114
+ raise ArgumentError, "EM::Mongo::Collection#find does not support disabling the timeout"
26
115
  end
116
+
117
+ if hint
118
+ hint = normalize_hint_fields(hint)
119
+ end
120
+
121
+ raise RuntimeError, "Unknown options [#{opts.inspect}]" unless opts.empty?
122
+
123
+ EM::Mongo::Cursor.new(self, {
124
+ :selector => selector,
125
+ :fields => fields,
126
+ :skip => skip,
127
+ :limit => limit,
128
+ :order => sort,
129
+ :hint => hint,
130
+ :snapshot => snapshot,
131
+ :timeout => timeout,
132
+ :batch_size => batch_size,
133
+ :transformer => transformer,
134
+ :max_scan => max_scan,
135
+ :show_disk_loc => show_disk_loc,
136
+ :return_key => return_key
137
+ })
27
138
  end
28
139
 
29
- def insert(doc)
30
- sanitize_id!(doc)
31
- @connection.insert(@name, doc)
32
- doc[:_id] # mongo-ruby-driver returns ID
140
+ # Return a single object from the database.
141
+ #
142
+ # @return [EM::Mongo::RequestResponse]
143
+ # calls back with a single document or nil if no result is found.
144
+ #
145
+ # @param [Hash, ObjectId, Nil] spec_or_object_id a hash specifying elements
146
+ # which must be present for a document to be included in the result set or an
147
+ # instance of ObjectId to be used as the value for an _id query.
148
+ # If nil, an empty selector, {}, will be used.
149
+ #
150
+ # @option opts [Hash]
151
+ # any valid options that can be send to Collection#find
152
+ #
153
+ # @raise [TypeError]
154
+ # if the argument is of an improper type.
155
+ def find_one(spec_or_object_id=nil, opts={})
156
+ spec = case spec_or_object_id
157
+ when nil
158
+ {}
159
+ when BSON::ObjectId
160
+ {:_id => spec_or_object_id}
161
+ when Hash
162
+ spec_or_object_id
163
+ else
164
+ raise TypeError, "spec_or_object_id must be an instance of ObjectId or Hash, or nil"
165
+ end
166
+ find(spec, opts.merge(:limit => -1)).next_document
33
167
  end
168
+ alias :first :find_one
34
169
 
35
- def update(selector, updater, opts={})
36
- @connection.update(@name, selector, updater, opts)
37
- true
170
+ # Insert one or more documents into the collection.
171
+ #
172
+ # @param [Hash, Array] doc_or_docs
173
+ # a document (as a hash) or array of documents to be inserted.
174
+ #
175
+ # @return [ObjectId, Array]
176
+ # The _id of the inserted document or a list of _ids of all inserted documents.
177
+ #
178
+ # @see DB#remove for options that can be passed to :safe.
179
+ #
180
+ # @core insert insert-instance_method
181
+ def insert(doc_or_docs)
182
+ safe_insert(doc_or_docs, :safe => false).data
183
+ end
184
+ alias_method :<<, :insert
185
+
186
+ # Insert one or more documents into the collection, with a failure if the operation doesn't succeed
187
+ # Unlike insert, this method returns a deferrable
188
+ #
189
+ # @param [Hash, Array] doc_or_docs
190
+ # a document (as a hash) or array of documents to be inserted.
191
+ #
192
+ # @return [EM::Mongo::RequestResponse]
193
+ # Calls backw ith the _id of the inserted document or a list of _ids of all inserted documents.
194
+ #
195
+ # @option opts [Boolean, Hash] :safe (+true+)
196
+ # run the operation in safe mode, which run a getlasterror command on the
197
+ # database to report any assertion. In addition, a hash can be provided to
198
+ # run an fsync and/or wait for replication of the insert (>= 1.5.1). Safe
199
+ # options provided here will override any safe options set on this collection,
200
+ # its database object, or the current connection. See the options on
201
+ # for DB#get_last_error.
202
+ #
203
+ # @see DB#remove for options that can be passed to :safe.
204
+ #
205
+ # @core insert insert-instance_method
206
+ def safe_insert(doc_or_docs, safe_opts = {})
207
+ response = RequestResponse.new
208
+ safe_opts[:safe] = true unless safe_opts[:safe] == false
209
+ doc_or_docs = [doc_or_docs] unless doc_or_docs.is_a?(Array)
210
+ doc_or_docs.map! { |doc| sanitize_id!(doc) }
211
+ insert_resp = insert_documents(doc_or_docs, @ns, true, safe_opts)
212
+ insert_resp.callback do |ids|
213
+ ids.length > 1 ? response.succeed(ids) : response.succeed(ids[0])
214
+ end
215
+ insert_resp.errback do |err|
216
+ response.fail err
217
+ end
218
+ response
219
+ end
220
+
221
+ # Update one or more documents in this collection.
222
+ #
223
+ # @param [Hash] selector
224
+ # a hash specifying elements which must be present for a document to be updated. Note:
225
+ # the update command currently updates only the first document matching the
226
+ # given selector. If you want all matching documents to be updated, be sure
227
+ # to specify :multi => true.
228
+ # @param [Hash] document
229
+ # a hash specifying the fields to be changed in the selected document,
230
+ # or (in the case of an upsert) the document to be inserted
231
+ #
232
+ # @option opts [Boolean] :upsert (+false+) if true, performs an upsert (update or insert)
233
+ # @option opts [Boolean] :multi (+false+) update all documents matching the selector, as opposed to
234
+ # just the first matching document. Note: only works in MongoDB 1.1.3 or later.
235
+ #
236
+ # @return [Hash, true] Returns a Hash containing the last error object if running in safe mode.
237
+ # Otherwise, returns true.
238
+ #
239
+ # @core update update-instance_method
240
+ def update(selector, document, opts={})
241
+ # Initial byte is 0.
242
+ safe_update(selector, document, opts.merge(:safe => false)).data
243
+ end
244
+
245
+ # Update one or more documents in this collection.
246
+ #
247
+ # @param [Hash] selector
248
+ # a hash specifying elements which must be present for a document to be updated. Note:
249
+ # the update command currently updates only the first document matching the
250
+ # given selector. If you want all matching documents to be updated, be sure
251
+ # to specify :multi => true.
252
+ # @param [EM::Mongo::RequestResponse] document
253
+ # calls back with a hash specifying the fields to be changed in the selected document,
254
+ # or (in the case of an upsert) the document to be inserted
255
+ #
256
+ # @option opts [Boolean] :upsert (+false+) if true, performs an upsert (update or insert)
257
+ # @option opts [Boolean] :multi (+false+) update all documents matching the selector, as opposed to
258
+ # just the first matching document. Note: only works in MongoDB 1.1.3 or later.
259
+ # @option opts [Boolean] :safe (+true+)
260
+ # If true, check that the save succeeded. OperationFailure
261
+ # will be raised on an error. Note that a safe check requires an extra
262
+ # round-trip to the database. Safe options provided here will override any safe
263
+ # options set on this collection, its database object, or the current collection.
264
+ # See the options for DB#get_last_error for details.
265
+ #
266
+ # @return [Hash, true] Returns a Hash containing the last error object if running in safe mode.
267
+ # Otherwise, returns true.
268
+ #
269
+ # @core update update-instance_method
270
+ def safe_update(selector, document, opts={})
271
+ response = RequestResponse.new
272
+ opts = opts.dup
273
+ opts[:safe] = true unless opts[:safe] == false
274
+ # Initial byte is 0.
275
+ message = BSON::ByteBuffer.new("\0\0\0\0")
276
+ BSON::BSON_RUBY.serialize_cstr(message, "#{@db}.#{@ns}")
277
+ update_options = 0
278
+ update_options += 1 if opts.delete(:upsert)
279
+ update_options += 2 if opts.delete(:multi)
280
+ message.put_int(update_options)
281
+ message.put_binary(BSON::BSON_CODER.serialize(selector, false, true).to_s)
282
+ message.put_binary(BSON::BSON_CODER.serialize(document, false, true).to_s)
283
+
284
+ if opts[:safe]
285
+ send_resp = safe_send(EM::Mongo::OP_UPDATE, message, true, opts)
286
+ send_resp.callback { response.succeed(true) }
287
+ send_resp.errback { |err| response.fail(err) }
288
+ else
289
+ @connection.send_command(EM::Mongo::OP_UPDATE, message)
290
+ response.succeed(true)
291
+ end
292
+ response
38
293
  end
39
294
 
40
- # XXX Missing tests
295
+ # Save a document to this collection.
296
+ #
297
+ # @param [Hash] doc
298
+ # the document to be saved. If the document already has an '_id' key,
299
+ # then an update (upsert) operation will be performed, and any existing
300
+ # document with that _id is overwritten. Otherwise an insert operation is performed.
301
+ #
302
+ # @return [ObjectId] the _id of the saved document.
303
+ #
41
304
  def save(doc, opts={})
305
+ safe_save(doc, opts.merge(:safe => false)).data
306
+ end
307
+
308
+ # Save a document to this collection.
309
+ #
310
+ # @param [Hash] doc
311
+ # the document to be saved. If the document already has an '_id' key,
312
+ # then an update (upsert) operation will be performed, and any existing
313
+ # document with that _id is overwritten. Otherwise an insert operation is performed.
314
+ #
315
+ # @return [EM::Mongo::RequestResponse] Calls backw with the _id of the saved document.
316
+ #
317
+ # @option opts [Boolean, Hash] :safe (+true+)
318
+ # run the operation in safe mode, which run a getlasterror command on the
319
+ # database to report any assertion. In addition, a hash can be provided to
320
+ # run an fsync and/or wait for replication of the save (>= 1.5.1). See the options
321
+ # for DB#error.
322
+ #
323
+ def safe_save(doc, opts={})
324
+ opts[:safe] = true unless opts[:safe] == false
42
325
  id = has_id?(doc)
43
326
  sanitize_id!(doc)
44
327
  if id
45
- update({:_id => id}, doc, :upsert => true)
46
- id
328
+ safe_update({:_id => id}, doc, opts.merge(:upsert => true))
47
329
  else
48
- insert(doc)
330
+ safe_insert(doc, opts)
49
331
  end
50
332
  end
51
333
 
52
- def remove(obj = {})
53
- @connection.delete(@name, obj)
334
+ # Remove all documents from this collection.
335
+ #
336
+ # @param [Hash] selector
337
+ # If specified, only matching documents will be removed.
338
+ #
339
+ # @option opts [Boolean, Hash] :safe (+false+)
340
+ # run the operation in safe mode, which will run a getlasterror command on the
341
+ # database to report any assertion. In addition, a hash can be provided to
342
+ # run an fsync and/or wait for replication of the remove (>= 1.5.1). Safe
343
+ # options provided here will override any safe options set on this collection,
344
+ # its database, or the current connection. See the options for DB#get_last_error for more details.
345
+ #
346
+ # @example remove all documents from the 'users' collection:
347
+ # users.remove
348
+ # users.remove({})
349
+ #
350
+ # @example remove only documents that have expired:
351
+ # users.remove({:expire => {"$lte" => Time.now}})
352
+ #
353
+ # @return [true] Returns true.
354
+ #
355
+ # @see DB#remove for options that can be passed to :safe.
356
+ #
357
+ # @core remove remove-instance_method
358
+ def remove(selector={}, opts={})
359
+ # Initial byte is 0.
360
+ message = BSON::ByteBuffer.new("\0\0\0\0")
361
+ BSON::BSON_RUBY.serialize_cstr(message, "#{@db}.#{@ns}")
362
+ message.put_int(0)
363
+ message.put_binary(BSON::BSON_CODER.serialize(selector, false, true).to_s)
364
+ @connection.send_command(EM::Mongo::OP_DELETE, message)
54
365
  true
55
366
  end
56
367
 
368
+ # Drop the entire collection. USE WITH CAUTION.
369
+ def drop
370
+ db.drop_collection(@ns)
371
+ end
372
+
373
+ # Atomically update and return a document using MongoDB's findAndModify command. (MongoDB > 1.3.0)
374
+ #
375
+ # @option opts [Hash] :query ({}) a query selector document for matching the desired document.
376
+ # @option opts [Hash] :update (nil) the update operation to perform on the matched document.
377
+ # @option opts [Array, String, OrderedHash] :sort ({}) specify a sort option for the query using any
378
+ # of the sort options available for Cursor#sort. Sort order is important if the query will be matching
379
+ # multiple documents since only the first matching document will be updated and returned.
380
+ # @option opts [Boolean] :remove (false) If true, removes the the returned document from the collection.
381
+ # @option opts [Boolean] :new (false) If true, returns the updated document; otherwise, returns the document
382
+ # prior to update.
383
+ #
384
+ # @return [EM::Mongo::RequestResponse] Calls back with the matched document.
385
+ #
386
+ # @core findandmodify find_and_modify-instance_method
387
+ def find_and_modify(opts={})
388
+ response = RequestResponse.new
389
+ cmd = BSON::OrderedHash.new
390
+ cmd[:findandmodify] = @ns
391
+ cmd.merge!(opts)
392
+ cmd[:sort] = EM::Mongo::Support.format_order_clause(opts[:sort]) if opts[:sort]
393
+
394
+ cmd_resp = db.command(cmd)
395
+ cmd_resp.callback do |doc|
396
+ response.succeed doc['value']
397
+ end
398
+ cmd_resp.errback do |err|
399
+ response.fail err
400
+ end
401
+ response
402
+ end
403
+
404
+ # Perform a map-reduce operation on the current collection.
405
+ #
406
+ # @param [String, BSON::Code] map a map function, written in JavaScript.
407
+ # @param [String, BSON::Code] reduce a reduce function, written in JavaScript.
408
+ #
409
+ # @option opts [Hash] :query ({}) a query selector document, like what's passed to #find, to limit
410
+ # the operation to a subset of the collection.
411
+ # @option opts [Array] :sort ([]) an array of [key, direction] pairs to sort by. Direction should
412
+ # be specified as Mongo::ASCENDING (or :ascending / :asc) or Mongo::DESCENDING (or :descending / :desc)
413
+ # @option opts [Integer] :limit (nil) if passing a query, number of objects to return from the collection.
414
+ # @option opts [String, BSON::Code] :finalize (nil) a javascript function to apply to the result set after the
415
+ # map/reduce operation has finished.
416
+ # @option opts [String] :out (nil) a valid output type. In versions of MongoDB prior to v1.7.6,
417
+ # this option takes the name of a collection for the output results. In versions 1.7.6 and later,
418
+ # this option specifies the output type. See the core docs for available output types.
419
+ # @option opts [Boolean] :keeptemp (false) if true, the generated collection will be persisted. The defualt
420
+ # is false. Note that this option has no effect is versions of MongoDB > v1.7.6.
421
+ # @option opts [Boolean ] :verbose (false) if true, provides statistics on job execution time.
422
+ # @option opts [Boolean] :raw (false) if true, return the raw result object from the map_reduce command, and not
423
+ # the instantiated collection that's returned by default. Note if a collection name isn't returned in the
424
+ # map-reduce output (as, for example, when using :out => {:inline => 1}), then you must specify this option
425
+ # or an ArgumentError will be raised.
426
+ #
427
+ # @return [EM::Mongo::RequestResponse] Calls back with a EM::Mongo::Collection object or a Hash with the map-reduce command's results.
428
+ #
429
+ # @raise ArgumentError if you specify {:out => {:inline => true}} but don't specify :raw => true.
430
+ #
431
+ # @see http://www.mongodb.org/display/DOCS/MapReduce Offical MongoDB map/reduce documentation.
432
+ #
433
+ # @core mapreduce map_reduce-instance_method
434
+ def map_reduce(map, reduce, opts={})
435
+ response = RequestResponse.new
436
+ map = BSON::Code.new(map) unless map.is_a?(BSON::Code)
437
+ reduce = BSON::Code.new(reduce) unless reduce.is_a?(BSON::Code)
438
+ raw = opts.delete(:raw)
439
+
440
+ hash = BSON::OrderedHash.new
441
+ hash['mapreduce'] = @ns
442
+ hash['map'] = map
443
+ hash['reduce'] = reduce
444
+ hash.merge! opts
445
+
446
+ cmd_resp = db.command(hash)
447
+ cmd_resp.callback do |result|
448
+ if EM::Mongo::Support.ok?(result) == false
449
+ response.fail [Mongo::OperationFailure, "map-reduce failed: #{result['errmsg']}"]
450
+ elsif raw
451
+ response.succeed result
452
+ elsif result["result"]
453
+ response.succeed db.collection(result["result"])
454
+ else
455
+ response.fail [ArgumentError, "Could not instantiate collection from result. If you specified " +
456
+ "{:out => {:inline => true}}, then you must also specify :raw => true to get the results."]
457
+ end
458
+ end
459
+ cmd_resp.errback do |err|
460
+ response.fail(err)
461
+ end
462
+ response
463
+ end
464
+ alias :mapreduce :map_reduce
465
+
466
+ # Return a list of distinct values for +key+ across all
467
+ # documents in the collection. The key may use dot notation
468
+ # to reach into an embedded object.
469
+ #
470
+ # @param [String, Symbol, OrderedHash] key or hash to group by.
471
+ # @param [Hash] query a selector for limiting the result set over which to group.
472
+ #
473
+ # @example Saving zip codes and ages and returning distinct results.
474
+ # @collection.save({:zip => 10010, :name => {:age => 27}})
475
+ # @collection.save({:zip => 94108, :name => {:age => 24}})
476
+ # @collection.save({:zip => 10010, :name => {:age => 27}})
477
+ # @collection.save({:zip => 99701, :name => {:age => 24}})
478
+ # @collection.save({:zip => 94108, :name => {:age => 27}})
479
+ #
480
+ # @collection.distinct(:zip)
481
+ # [10010, 94108, 99701]
482
+ # @collection.distinct("name.age")
483
+ # [27, 24]
484
+ #
485
+ # # You may also pass a document selector as the second parameter
486
+ # # to limit the documents over which distinct is run:
487
+ # @collection.distinct("name.age", {"name.age" => {"$gt" => 24}})
488
+ # [27]
489
+ #
490
+ # @return [EM::Mongo::RequestResponse] Calls back with an array of distinct values.
491
+ def distinct(key, query=nil)
492
+ raise MongoArgumentError unless [String, Symbol].include?(key.class)
493
+ response = RequestResponse.new
494
+ command = BSON::OrderedHash.new
495
+ command[:distinct] = @ns
496
+ command[:key] = key.to_s
497
+ command[:query] = query
498
+
499
+ cmd_resp = db.command(command)
500
+ cmd_resp.callback do |resp|
501
+ response.succeed resp["values"]
502
+ end
503
+ cmd_resp.errback do |err|
504
+ response.fail err
505
+ end
506
+ response
507
+ end
508
+
509
+ # Perform a group aggregation.
510
+ #
511
+ # @param [Hash] opts the options for this group operation. The minimum required are :initial
512
+ # and :reduce.
513
+ #
514
+ # @option opts [Array, String, Symbol] :key (nil) Either the name of a field or a list of fields to group by (optional).
515
+ # @option opts [String, BSON::Code] :keyf (nil) A JavaScript function to be used to generate the grouping keys (optional).
516
+ # @option opts [String, BSON::Code] :cond ({}) A document specifying a query for filtering the documents over
517
+ # which the aggregation is run (optional).
518
+ # @option opts [Hash] :initial the initial value of the aggregation counter object (required).
519
+ # @option opts [String, BSON::Code] :reduce (nil) a JavaScript aggregation function (required).
520
+ # @option opts [String, BSON::Code] :finalize (nil) a JavaScript function that receives and modifies
521
+ # each of the resultant grouped objects. Available only when group is run with command
522
+ # set to true.
523
+ #
524
+ # @return [EM::Mongo::RequestResponse] calls back with the command response consisting of grouped items.
525
+ def group(opts={})
526
+ response = RequestResponse.new
527
+ reduce = opts[:reduce]
528
+ finalize = opts[:finalize]
529
+ cond = opts.fetch(:cond, {})
530
+ initial = opts[:initial]
531
+
532
+ if !(reduce && initial)
533
+ raise MongoArgumentError, "Group requires at minimum values for initial and reduce."
534
+ end
535
+
536
+ cmd = {
537
+ "group" => {
538
+ "ns" => @ns,
539
+ "$reduce" => reduce.to_bson_code,
540
+ "cond" => cond,
541
+ "initial" => initial
542
+ }
543
+ }
544
+
545
+ if finalize
546
+ cmd['group']['finalize'] = finalize.to_bson_code
547
+ end
548
+
549
+ if key = opts[:key]
550
+ if key.is_a?(String) || key.is_a?(Symbol)
551
+ key = [key]
552
+ end
553
+ key_value = {}
554
+ key.each { |k| key_value[k] = 1 }
555
+ cmd["group"]["key"] = key_value
556
+ elsif keyf = opts[:keyf]
557
+ cmd["group"]["$keyf"] = keyf.to_bson_code
558
+ end
559
+
560
+ cmd_resp = db.command(cmd)
561
+ cmd_resp.callback do |result|
562
+ response.succeed result["retval"]
563
+ end
564
+ cmd_resp.errback do |err|
565
+ response.fail err
566
+ end
567
+ response
568
+ end
569
+
570
+
571
+ # Get the number of documents in this collection.
572
+ #
573
+ # @return [EM::Mongo::RequestResponse]
574
+ def count
575
+ find().count
576
+ end
577
+ alias :size :count
578
+
579
+ # Return stats on the collection. Uses MongoDB's collstats command.
580
+ #
581
+ # @return [EM::Mongo::RequestResponse]
582
+ def stats
583
+ @db.command({:collstats => @name})
584
+ end
585
+
586
+ # Get information on the indexes for this collection.
587
+ #
588
+ # @return [EM::Mongo::RequestResponse] Calls back with a hash where the keys are index names.
589
+ #
590
+ # @core indexes
591
+ def index_information
592
+ db.index_information(@ns)
593
+ end
594
+
595
+ # Create a new index.
596
+ #
597
+ # @param [String, Array] spec
598
+ # should be either a single field name or an array of
599
+ # [field name, direction] pairs. Directions should be specified
600
+ # as Mongo::ASCENDING, Mongo::DESCENDING, or Mongo::GEO2D.
601
+ #
602
+ # Note that geospatial indexing only works with versions of MongoDB >= 1.3.3+. Keep in mind, too,
603
+ # that in order to geo-index a given field, that field must reference either an array or a sub-object
604
+ # where the first two values represent x- and y-coordinates. Examples can be seen below.
605
+ #
606
+ # Also note that it is permissible to create compound indexes that include a geospatial index as
607
+ # long as the geospatial index comes first.
608
+ #
609
+ # If your code calls create_index frequently, you can use Collection#ensure_index to cache these calls
610
+ # and thereby prevent excessive round trips to the database.
611
+ #
612
+ # @option opts [Boolean] :unique (false) if true, this index will enforce a uniqueness constraint.
613
+ # @option opts [Boolean] :background (false) indicate that the index should be built in the background. This
614
+ # feature is only available in MongoDB >= 1.3.2.
615
+ # @option opts [Boolean] :drop_dups (nil) If creating a unique index on a collection with pre-existing records,
616
+ # this option will keep the first document the database indexes and drop all subsequent with duplicate values.
617
+ # @option opts [Integer] :min (nil) specify the minimum longitude and latitude for a geo index.
618
+ # @option opts [Integer] :max (nil) specify the maximum longitude and latitude for a geo index.
619
+ #
620
+ # @example Creating a compound index:
621
+ # @posts.create_index([['subject', Mongo::ASCENDING], ['created_at', Mongo::DESCENDING]])
622
+ #
623
+ # @example Creating a geospatial index:
624
+ # @restaurants.create_index([['location', Mongo::GEO2D]])
625
+ #
626
+ # # Note that this will work only if 'location' represents x,y coordinates:
627
+ # {'location': [0, 50]}
628
+ # {'location': {'x' => 0, 'y' => 50}}
629
+ # {'location': {'latitude' => 0, 'longitude' => 50}}
630
+ #
631
+ # @example A geospatial index with alternate longitude and latitude:
632
+ # @restaurants.create_index([['location', Mongo::GEO2D]], :min => 500, :max => 500)
633
+ #
634
+ # @return [String] the name of the index created.
635
+ #
636
+ # @core indexes create_index-instance_method
637
+ def create_index(spec, opts={})
638
+ field_spec = parse_index_spec(spec)
639
+ opts = opts.dup
640
+ name = opts.delete(:name) || generate_index_name(field_spec)
641
+ name = name.to_s if name
642
+
643
+ generate_indexes(field_spec, name, opts)
644
+ name
645
+ end
646
+
647
+ # Drop a specified index.
648
+ #
649
+ # @param [EM::Mongo::RequestResponse] name
650
+ #
651
+ # @core indexes
652
+ def drop_index(name)
653
+ if name.is_a?(Array)
654
+ response = RequestResponse.new
655
+ name_resp = index_name(name)
656
+ name_resp.callback do |name|
657
+ drop_resp = db.drop_index(@ns, name)
658
+ drop_resp.callback { response.succeed }
659
+ drop_resp.errback { |err| response.fail(err) }
660
+ end
661
+ name_resp.errback { |err| response.fail(err) }
662
+ response
663
+ else
664
+ db.drop_index(@ns, name)
665
+ end
666
+ end
667
+
668
+ # Drop all indexes.
669
+ #
670
+ # @core indexes
671
+ def drop_indexes
672
+ # Note: calling drop_indexes with no args will drop them all.
673
+ db.drop_index(@ns, '*')
674
+ end
675
+
676
+ protected
677
+
678
+ def normalize_hint_fields(hint)
679
+ case hint
680
+ when String
681
+ {hint => 1}
682
+ when Hash
683
+ hint
684
+ when nil
685
+ nil
686
+ else
687
+ h = BSON::OrderedHash.new
688
+ hint.to_a.each { |k| h[k] = 1 }
689
+ h
690
+ end
691
+ end
692
+
57
693
  private
58
694
 
59
695
  def has_id?(doc)
@@ -69,5 +705,102 @@ module EM::Mongo
69
705
  doc
70
706
  end
71
707
 
708
+ # Sends a Mongo::Constants::OP_INSERT message to the database.
709
+ # Takes an array of +documents+, an optional +collection_name+, and a
710
+ # +check_keys+ setting.
711
+ def insert_documents(documents, collection_name=@name, check_keys = true, safe_options={})
712
+ response = RequestResponse.new
713
+ # Initial byte is 0.
714
+ message = BSON::ByteBuffer.new("\0\0\0\0")
715
+ BSON::BSON_RUBY.serialize_cstr(message, "#{@db}.#{collection_name}")
716
+ documents.each do |doc|
717
+ message.put_binary(BSON::BSON_CODER.serialize(doc, check_keys, true).to_s)
718
+ end
719
+ raise InvalidOperation, "Exceded maximum insert size of 16,000,000 bytes" if message.size > 16_000_000
720
+
721
+ ids = documents.collect { |o| o[:_id] || o['_id'] }
722
+
723
+ if safe_options[:safe]
724
+ send_resp = safe_send(EM::Mongo::OP_INSERT, message, ids, safe_options)
725
+ send_resp.callback { response.succeed(ids) }
726
+ send_resp.errback { |err| response.fail(err) }
727
+ else
728
+ @connection.send_command(EM::Mongo::OP_INSERT, message)
729
+ response.succeed(ids)
730
+ end
731
+ response
732
+ end
733
+
734
+ def safe_send(op, message, return_val, options={})
735
+ response = RequestResponse.new
736
+ options[:safe] = true
737
+ options[:db_name] = @db
738
+ @connection.send_command(op, message, options) do |server_resp|
739
+ docs = server_resp.docs
740
+ if server_resp.number_returned == 1 && (error = docs[0]['err'] || docs[0]['errmsg'])
741
+ @connection.close if error == "not master"
742
+ error = "wtimeout" if error == "timeout"
743
+ response.fail [EM::Mongo::OperationFailure, "#{docs[0]['code']}: #{error}"]
744
+ else
745
+ response.succeed(return_val)
746
+ end
747
+ end
748
+ response
749
+ end
750
+
751
+ def index_name(spec)
752
+ response = RequestResponse.new
753
+ field_spec = parse_index_spec(spec)
754
+ info_resp = index_information
755
+ info_resp.callback do |indexes|
756
+ found = indexes.values.find do |index|
757
+ index['key'] == field_spec
758
+ end
759
+ response.succeed( found ? found['name'] : nil )
760
+ end
761
+ info_resp.errback do |err|
762
+ response.fail err
763
+ end
764
+ response
765
+ end
766
+
767
+ def parse_index_spec(spec)
768
+ field_spec = BSON::OrderedHash.new
769
+ if spec.is_a?(String) || spec.is_a?(Symbol)
770
+ field_spec[spec.to_s] = 1
771
+ elsif spec.is_a?(Array) && spec.all? {|field| field.is_a?(Array) }
772
+ spec.each do |f|
773
+ if [EM::Mongo::ASCENDING, EM::Mongo::DESCENDING, EM::Mongo::GEO2D].include?(f[1])
774
+ field_spec[f[0].to_s] = f[1]
775
+ else
776
+ raise MongoArgumentError, "Invalid index field #{f[1].inspect}; " +
777
+ "should be one of Mongo::ASCENDING (1), Mongo::DESCENDING (-1) or Mongo::GEO2D ('2d')."
778
+ end
779
+ end
780
+ else
781
+ raise MongoArgumentError, "Invalid index specification #{spec.inspect}; " +
782
+ "should be either a string, symbol, or an array of arrays."
783
+ end
784
+ field_spec
785
+ end
786
+
787
+ def generate_indexes(field_spec, name, opts)
788
+ selector = {
789
+ :name => name,
790
+ :ns => "#{@db}.#{@ns}",
791
+ :key => field_spec
792
+ }
793
+ selector.merge!(opts)
794
+ insert_documents([selector], EM::Mongo::Database::SYSTEM_INDEX_COLLECTION, false)
795
+ end
796
+
797
+ def generate_index_name(spec)
798
+ indexes = []
799
+ spec.each_pair do |field, direction|
800
+ indexes.push("#{field}_#{direction}")
801
+ end
802
+ indexes.join("_")
803
+ end
804
+
72
805
  end
73
806
  end