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 +1 -1
- data/lib/em-mongo/collection.rb +756 -23
- data/lib/em-mongo/connection.rb +100 -95
- data/lib/em-mongo/conversions.rb +2 -2
- data/lib/em-mongo/core_ext.rb +20 -0
- data/lib/em-mongo/cursor.rb +537 -0
- data/lib/em-mongo/database.rb +348 -20
- data/lib/em-mongo/exceptions.rb +2 -2
- data/lib/em-mongo/prev.rb +53 -0
- data/lib/em-mongo/request_response.rb +34 -0
- data/lib/em-mongo/server_response.rb +32 -0
- data/lib/em-mongo/support.rb +4 -4
- data/lib/em-mongo.rb +5 -0
- data/spec/integration/collection_spec.rb +654 -154
- data/spec/integration/cursor_spec.rb +350 -0
- data/spec/integration/database_spec.rb +149 -3
- data/spec/integration/request_response_spec.rb +63 -0
- metadata +12 -2
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.4.0
|
data/lib/em-mongo/collection.rb
CHANGED
@@ -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
|
-
|
13
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
25
|
+
#The name of this collection
|
26
|
+
# @return [String]
|
27
|
+
def name
|
28
|
+
@ns
|
29
|
+
end
|
18
30
|
|
19
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
36
|
-
|
37
|
-
|
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
|
-
#
|
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
|
-
|
46
|
-
id
|
328
|
+
safe_update({:_id => id}, doc, opts.merge(:upsert => true))
|
47
329
|
else
|
48
|
-
|
330
|
+
safe_insert(doc, opts)
|
49
331
|
end
|
50
332
|
end
|
51
333
|
|
52
|
-
|
53
|
-
|
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
|