em-mongo 0.3.6 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION 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