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 +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
|