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/lib/em-mongo/database.rb
CHANGED
@@ -8,51 +8,379 @@ module EM::Mongo
|
|
8
8
|
SYSTEM_JS_COLLECTION = "system.js"
|
9
9
|
SYSTEM_COMMAND_COLLECTION = "$cmd"
|
10
10
|
|
11
|
+
# The length of time that Collection.ensure_index should cache index calls
|
12
|
+
attr_accessor :cache_time
|
13
|
+
|
14
|
+
# @param [String] name the database name.
|
15
|
+
# @param [EM::Mongo::Connection] connection a connection object pointing to MongoDB. Note
|
16
|
+
# that databases are usually instantiated via the Connection class. See the examples below.
|
17
|
+
#
|
18
|
+
# @core databases constructor_details
|
11
19
|
def initialize(name = DEFAULT_DB, connection = nil)
|
12
20
|
@db_name = name
|
13
21
|
@em_connection = connection || EM::Mongo::Connection.new
|
14
22
|
@collection = nil
|
15
23
|
@collections = {}
|
24
|
+
@cache_time = 300 #5 minutes.
|
16
25
|
end
|
17
26
|
|
27
|
+
# Get a collection by name.
|
28
|
+
#
|
29
|
+
# @param [String, Symbol] name the collection name.
|
30
|
+
#
|
31
|
+
# @return [EM::Mongo::Collection]
|
18
32
|
def collection(name = EM::Mongo::DEFAULT_NS)
|
19
33
|
@collections[name] ||= EM::Mongo::Collection.new(@db_name, name, @em_connection)
|
20
34
|
end
|
21
35
|
|
36
|
+
# Get the connection associated with this database
|
37
|
+
#
|
38
|
+
# @return [EM::Mongo::Connection]
|
22
39
|
def connection
|
23
40
|
@em_connection
|
24
41
|
end
|
25
42
|
|
26
|
-
|
27
|
-
|
43
|
+
#Get the name of this database
|
44
|
+
#
|
45
|
+
# @return [String]
|
46
|
+
def name
|
47
|
+
@db_name
|
28
48
|
end
|
29
49
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
50
|
+
# Get an array of collection names in this database.
|
51
|
+
#
|
52
|
+
# @return [EM::Mongo::RequestResponse]
|
53
|
+
def collection_names
|
54
|
+
response = RequestResponse.new
|
55
|
+
name_resp = collections_info.to_a
|
56
|
+
name_resp.callback do |docs|
|
57
|
+
names = docs.collect{ |doc| doc['name'] || '' }
|
58
|
+
names = names.delete_if {|name| name.index(self.name).nil? || name.index('$')}
|
59
|
+
names = names.map{ |name| name.sub(self.name + '.','')}
|
60
|
+
response.succeed(names)
|
61
|
+
end
|
62
|
+
name_resp.errback { |err| response.fail err }
|
63
|
+
response
|
64
|
+
end
|
65
|
+
|
66
|
+
# Get an array of Collection instances, one for each collection in this database.
|
67
|
+
#
|
68
|
+
# @return [EM::Mongo::RequestResponse]
|
69
|
+
def collections
|
70
|
+
response = RequestResponse.new
|
71
|
+
name_resp = collection_names
|
72
|
+
name_resp.callback do |names|
|
73
|
+
response.succeed names.map do |name|
|
74
|
+
EM::Mongo::Collection.new(@db_name, name, @em_connection)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
name_resp.errback { |err| response.fail err }
|
78
|
+
response
|
79
|
+
end
|
80
|
+
|
81
|
+
# Get info on system namespaces (collections). This method returns
|
82
|
+
# a cursor which can be iterated over. For each collection, a hash
|
83
|
+
# will be yielded containing a 'name' string and, optionally, an 'options' hash.
|
84
|
+
#
|
85
|
+
# @param [String] coll_name return info for the specifed collection only.
|
86
|
+
#
|
87
|
+
# @return [EM::Mongo::Cursor]
|
88
|
+
def collections_info(coll_name=nil)
|
89
|
+
selector = {}
|
90
|
+
selector[:name] = full_collection_name(coll_name) if coll_name
|
91
|
+
Cursor.new(EM::Mongo::Collection.new(@db_name, SYSTEM_NAMESPACE_COLLECTION, @em_connection), :selector => selector)
|
92
|
+
end
|
93
|
+
|
94
|
+
# Create a collection.
|
95
|
+
#
|
96
|
+
# new collection. If +strict+ is true, will raise an error if
|
97
|
+
# collection +name+ already exists.
|
98
|
+
#
|
99
|
+
# @param [String, Symbol] name the name of the new collection.
|
100
|
+
#
|
101
|
+
# @option opts [Boolean] :capped (False) created a capped collection.
|
102
|
+
#
|
103
|
+
# @option opts [Integer] :size (Nil) If +capped+ is +true+,
|
104
|
+
# specifies the maximum number of bytes for the capped collection.
|
105
|
+
# If +false+, specifies the number of bytes allocated
|
106
|
+
# for the initial extent of the collection.
|
107
|
+
#
|
108
|
+
# @option opts [Integer] :max (Nil) If +capped+ is +true+, indicates
|
109
|
+
# the maximum number of records in a capped collection.
|
110
|
+
#
|
111
|
+
# @raise [MongoDBError] raised under two conditions:
|
112
|
+
# either we're in +strict+ mode and the collection
|
113
|
+
# already exists or collection creation fails on the server.
|
114
|
+
#
|
115
|
+
# @return [EM::Mongo::RequestResponse] Calls back with the new collection
|
116
|
+
def create_collection(name)
|
117
|
+
response = RequestResponse.new
|
118
|
+
names_resp = collection_names
|
119
|
+
names_resp.callback do |names|
|
120
|
+
if names.include?(name.to_s)
|
121
|
+
response.succeed EM::Mongo::Collection.new(@db_name, name, @em_connection)
|
122
|
+
end
|
123
|
+
|
124
|
+
# Create a new collection.
|
125
|
+
oh = BSON::OrderedHash.new
|
126
|
+
oh[:create] = name
|
127
|
+
cmd_resp = command(oh)
|
128
|
+
cmd_resp.callback do |doc|
|
129
|
+
if EM::Mongo::Support.ok?(doc)
|
130
|
+
response.succeed EM::Mongo::Collection.new(@db_name, name, @em_connection)
|
43
131
|
else
|
44
|
-
|
132
|
+
response.fail [MongoDBError, "Error creating collection: #{doc.inspect}"]
|
133
|
+
end
|
134
|
+
end
|
135
|
+
cmd_resp.errback { |err| response.fail err }
|
136
|
+
end
|
137
|
+
names_resp.errback { |err| response.fail err }
|
138
|
+
response
|
139
|
+
end
|
140
|
+
|
141
|
+
# Drop a collection by +name+.
|
142
|
+
#
|
143
|
+
# @param [String, Symbol] name
|
144
|
+
#
|
145
|
+
# @return [EM::Mongo::RequestResponse] Calls back with +true+ on success or +false+ if the collection name doesn't exist.
|
146
|
+
def drop_collection(name)
|
147
|
+
response = RequestResponse.new
|
148
|
+
names_resp = collection_names
|
149
|
+
names_resp.callback do |names|
|
150
|
+
if names.include?(name.to_s)
|
151
|
+
cmd_resp = command(:drop=>name)
|
152
|
+
cmd_resp.callback do |doc|
|
153
|
+
response.succeed EM::Mongo::Support.ok?(doc)
|
154
|
+
end
|
155
|
+
cmd_resp.errback { |err| response.fail err }
|
156
|
+
else
|
157
|
+
response.succeed false
|
158
|
+
end
|
159
|
+
end
|
160
|
+
names_resp.errback { |err| response.fail err }
|
161
|
+
response
|
162
|
+
end
|
163
|
+
|
164
|
+
# Drop an index from a given collection. Normally called from
|
165
|
+
# Collection#drop_index or Collection#drop_indexes.
|
166
|
+
#
|
167
|
+
# @param [String] collection_name
|
168
|
+
# @param [String] index_name
|
169
|
+
#
|
170
|
+
# @return [EM::Mongo::RequestResponse] returns +true+ on success.
|
171
|
+
#
|
172
|
+
# @raise MongoDBError if there's an error renaming the collection.
|
173
|
+
def drop_index(collection_name, index_name)
|
174
|
+
response = RequestResponse.new
|
175
|
+
oh = BSON::OrderedHash.new
|
176
|
+
oh[:deleteIndexes] = collection_name
|
177
|
+
oh[:index] = index_name.to_s
|
178
|
+
cmd_resp = command(oh, :check_response => false)
|
179
|
+
cmd_resp.callback do |doc|
|
180
|
+
if EM::Mongo::Support.ok?(doc)
|
181
|
+
response.succeed(true)
|
182
|
+
else
|
183
|
+
response.fail [MongoDBError, "Error with drop_index command: #{doc.inspect}"]
|
184
|
+
end
|
185
|
+
end
|
186
|
+
cmd_resp.errback do |err|
|
187
|
+
response.fail err
|
188
|
+
end
|
189
|
+
response
|
190
|
+
end
|
191
|
+
|
192
|
+
# Get information on the indexes for the given collection.
|
193
|
+
# Normally called by Collection#index_information.
|
194
|
+
#
|
195
|
+
# @param [String] collection_name
|
196
|
+
#
|
197
|
+
# @return [EM::Mongo::RequestResponse] Calls back with a hash where keys are index names and the values are lists of [key, direction] pairs
|
198
|
+
# defining the index.
|
199
|
+
def index_information(collection_name)
|
200
|
+
response = RequestResponse.new
|
201
|
+
sel = {:ns => full_collection_name(collection_name)}
|
202
|
+
idx_resp = Cursor.new(self.collection(SYSTEM_INDEX_COLLECTION), :selector => sel).to_a
|
203
|
+
idx_resp.callback do |indexes|
|
204
|
+
info = indexes.inject({}) do |info, index|
|
205
|
+
info[index['name']] = index
|
206
|
+
info
|
207
|
+
end
|
208
|
+
response.succeed info
|
209
|
+
end
|
210
|
+
idx_resp.errback do |err|
|
211
|
+
fail err
|
212
|
+
end
|
213
|
+
response
|
214
|
+
end
|
215
|
+
|
216
|
+
# Run the getlasterror command with the specified replication options.
|
217
|
+
#
|
218
|
+
# @option opts [Boolean] :fsync (false)
|
219
|
+
# @option opts [Integer] :w (nil)
|
220
|
+
# @option opts [Integer] :wtimeout (nil)
|
221
|
+
#
|
222
|
+
# @return [EM::Mongo::RequestResponse] the entire response to getlasterror.
|
223
|
+
#
|
224
|
+
# @raise [MongoDBError] if the operation fails.
|
225
|
+
def get_last_error(opts={})
|
226
|
+
response = RequestResponse.new
|
227
|
+
cmd = BSON::OrderedHash.new
|
228
|
+
cmd[:getlasterror] = 1
|
229
|
+
cmd.merge!(opts)
|
230
|
+
cmd_resp = command(cmd, :check_response => false)
|
231
|
+
cmd_resp.callback do |doc|
|
232
|
+
if EM::Mongo::Support.ok?(doc)
|
233
|
+
response.succeed doc
|
234
|
+
else
|
235
|
+
response.fail [MongoDBError, "error retrieving last error: #{doc.inspect}"]
|
236
|
+
end
|
237
|
+
end
|
238
|
+
cmd_resp.errback { |err| response.fail err }
|
239
|
+
response
|
240
|
+
end
|
241
|
+
|
242
|
+
# Return +true+ if an error was caused by the most recently executed
|
243
|
+
# database operation.
|
244
|
+
#
|
245
|
+
# @return [EM::Mongo::RequestResponse]
|
246
|
+
def error?
|
247
|
+
response = RequestResponse.new
|
248
|
+
err_resp = get_last_error
|
249
|
+
err_resp.callback do |doc|
|
250
|
+
response.succeed doc['err'] != nil
|
251
|
+
end
|
252
|
+
err_resp.errback do |err|
|
253
|
+
response.fail err
|
254
|
+
end
|
255
|
+
response
|
256
|
+
end
|
257
|
+
|
258
|
+
# Reset the error history of this database
|
259
|
+
#
|
260
|
+
# Calls to DB#previous_error will only return errors that have occurred
|
261
|
+
# since the most recent call to this method.
|
262
|
+
#
|
263
|
+
# @return [EM::Mongo::RequestResponse]
|
264
|
+
def reset_error_history
|
265
|
+
command(:reseterror => 1)
|
266
|
+
end
|
267
|
+
|
268
|
+
|
269
|
+
# A shortcut returning db plus dot plus collection name.
|
270
|
+
#
|
271
|
+
# @param [String] collection_name
|
272
|
+
#
|
273
|
+
# @return [String]
|
274
|
+
def full_collection_name(collection_name)
|
275
|
+
"#{name}.#{collection_name}"
|
276
|
+
end
|
277
|
+
|
278
|
+
|
279
|
+
|
280
|
+
# Send a command to the database.
|
281
|
+
#
|
282
|
+
# Note: DB commands must start with the "command" key. For this reason,
|
283
|
+
# any selector containing more than one key must be an OrderedHash.
|
284
|
+
#
|
285
|
+
# Note also that a command in MongoDB is just a kind of query
|
286
|
+
# that occurs on the system command collection ($cmd). Examine this method's implementation
|
287
|
+
# to see how it works.
|
288
|
+
#
|
289
|
+
# @param [OrderedHash, Hash] selector an OrderedHash, or a standard Hash with just one
|
290
|
+
# key, specifying the command to be performed. In Ruby 1.9, OrderedHash isn't necessary since
|
291
|
+
# hashes are ordered by default.
|
292
|
+
#
|
293
|
+
# @option opts [Boolean] :check_response (true) If +true+, raises an exception if the
|
294
|
+
# command fails.
|
295
|
+
# @option opts [Socket] :socket a socket to use for sending the command. This is mainly for internal use.
|
296
|
+
#
|
297
|
+
# @return [EM::Mongo::RequestResponse] Calls back with a hash representing the result of the command
|
298
|
+
#
|
299
|
+
# @core commands command_instance-method
|
300
|
+
def command(selector, opts={})
|
301
|
+
check_response = opts.fetch(:check_response, true)
|
302
|
+
raise MongoArgumentError, "command must be given a selector" unless selector.is_a?(Hash) && !selector.empty?
|
303
|
+
|
304
|
+
if selector.keys.length > 1 && RUBY_VERSION < '1.9' && selector.class != BSON::OrderedHash
|
305
|
+
raise MongoArgumentError, "DB#command requires an OrderedHash when hash contains multiple keys"
|
306
|
+
end
|
307
|
+
|
308
|
+
response = RequestResponse.new
|
309
|
+
cmd_resp = Cursor.new(self.collection(SYSTEM_COMMAND_COLLECTION), :limit => -1, :selector => selector).next_document
|
310
|
+
|
311
|
+
cmd_resp.callback do |doc|
|
312
|
+
if doc.nil?
|
313
|
+
response.fail([OperationFailure, "Database command '#{selector.keys.first}' failed: returned null."])
|
314
|
+
elsif (check_response && !EM::Mongo::Support.ok?(doc))
|
315
|
+
response.fail([OperationFailure, "Database command '#{selector.keys.first}' failed: #{doc.inspect}"])
|
316
|
+
else
|
317
|
+
response.succeed(doc)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
cmd_resp.errback do |err|
|
322
|
+
response.fail([OperationFailure, "Database command '#{selector.keys.first}' failed: #{err[1]}"])
|
323
|
+
end
|
324
|
+
|
325
|
+
response
|
326
|
+
end
|
327
|
+
|
328
|
+
# Authenticate with the given username and password. Note that mongod
|
329
|
+
# must be started with the --auth option for authentication to be enabled.
|
330
|
+
#
|
331
|
+
# @param [String] username
|
332
|
+
# @param [String] password
|
333
|
+
#
|
334
|
+
# @return [EM::Mongo::RequestResponse] Calls back with +true+ or +false+, indicating success or failure
|
335
|
+
#
|
336
|
+
# @raise [AuthenticationError]
|
337
|
+
#
|
338
|
+
# @core authenticate authenticate-instance_method
|
339
|
+
def authenticate(username, password)
|
340
|
+
response = RequestResponse.new
|
341
|
+
auth_resp = self.collection(SYSTEM_COMMAND_COLLECTION).first({'getnonce' => 1})
|
342
|
+
auth_resp.callback do |res|
|
343
|
+
if not res or not res['nonce']
|
344
|
+
response.succeed false
|
345
|
+
else
|
346
|
+
auth = BSON::OrderedHash.new
|
347
|
+
auth['authenticate'] = 1
|
348
|
+
auth['user'] = username
|
349
|
+
auth['nonce'] = res['nonce']
|
350
|
+
auth['key'] = EM::Mongo::Support.auth_key(username, password, res['nonce'])
|
351
|
+
|
352
|
+
auth_resp2 = self.collection(SYSTEM_COMMAND_COLLECTION).first(auth)
|
353
|
+
auth_resp2.callback do |res|
|
354
|
+
if EM::Mongo::Support.ok?(res)
|
355
|
+
response.succeed true
|
356
|
+
else
|
357
|
+
response.fail res
|
358
|
+
end
|
45
359
|
end
|
360
|
+
auth_resp2.errback { |err| response.fail err }
|
46
361
|
end
|
47
362
|
end
|
363
|
+
auth_resp.errback { |err| response.fail err }
|
364
|
+
response
|
48
365
|
end
|
49
366
|
|
50
|
-
|
51
|
-
|
367
|
+
# Adds a user to this database for use with authentication. If the user already
|
368
|
+
# exists in the system, the password will be updated.
|
369
|
+
#
|
370
|
+
# @param [String] username
|
371
|
+
# @param [String] password
|
372
|
+
#
|
373
|
+
# @return [EM::Mongo::RequestResponse] Calls back with an object representing the user.
|
374
|
+
def add_user(username, password)
|
375
|
+
response = RequestResponse.new
|
376
|
+
user_resp = self.collection(SYSTEM_USER_COLLECTION).first({:user => username})
|
377
|
+
user_resp.callback do |res|
|
52
378
|
user = res || {:user => username}
|
53
|
-
user['pwd'] = Mongo::Support.hash_password(username, password)
|
54
|
-
|
379
|
+
user['pwd'] = EM::Mongo::Support.hash_password(username, password)
|
380
|
+
response.succeed self.collection(SYSTEM_USER_COLLECTION).save(user)
|
55
381
|
end
|
382
|
+
user_resp.errback { |err| response.fail err }
|
383
|
+
response
|
56
384
|
end
|
57
385
|
|
58
386
|
end
|
data/lib/em-mongo/exceptions.rb
CHANGED
@@ -0,0 +1,53 @@
|
|
1
|
+
module EM
|
2
|
+
module Mongo
|
3
|
+
class Collection
|
4
|
+
|
5
|
+
alias :new_find :find
|
6
|
+
def find(selector={}, opts={}, &blk)
|
7
|
+
raise "find requires a block" if not block_given?
|
8
|
+
|
9
|
+
new_find(selector, opts).to_a.callback do |docs|
|
10
|
+
blk.call(docs)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def first(selector={}, opts={}, &blk)
|
15
|
+
opts[:limit] = 1
|
16
|
+
find(selector, opts) do |res|
|
17
|
+
yield res.first
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class Connection
|
23
|
+
|
24
|
+
def insert(collection_name, documents)
|
25
|
+
db_name, col_name = db_and_col_name(collection_name)
|
26
|
+
db(db_name).collection(col_name).insert(documents)
|
27
|
+
end
|
28
|
+
|
29
|
+
def update(collection_name, selector, document, options={})
|
30
|
+
db_name, col_name = db_and_col_name(collection_name)
|
31
|
+
db(db_name).collection(col_name).update(selector, document, options)
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete(collection_name, selector)
|
35
|
+
db_name, col_name = db_and_col_name(collection_name)
|
36
|
+
db(db_name).collection(col_name).remove(selector)
|
37
|
+
end
|
38
|
+
|
39
|
+
def find(collection_name, skip, limit, order, query, fields, &blk)
|
40
|
+
db_name, col_name = db_and_col_name(collection_name)
|
41
|
+
db(db_name).collection(col_name).find(query, :skip=>skip,:limit=>limit,:order=>order,:fields=>fields).to_a.callback do |docs|
|
42
|
+
yield docs if block_given?
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def db_and_col_name(full_name)
|
47
|
+
parts = full_name.split(".")
|
48
|
+
[ parts.shift, parts.join(".") ]
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|