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