mongo 0.16 → 0.17

Sign up to get free protection for your applications and to get access to all the features.
@@ -356,6 +356,9 @@ Les Hill, leshill on github
356
356
  Sean Cribbs, seancribbs on github
357
357
  * Modify standard_benchmark to allow profiling
358
358
 
359
+ Sunny Hirai
360
+ * Suggested hashcode fix for Mongo::ObjectID
361
+
359
362
  = License
360
363
 
361
364
  Copyright 2008-2009 10gen Inc.
@@ -18,9 +18,16 @@ require 'mongo/cursor'
18
18
  require 'mongo/collection'
19
19
  require 'mongo/admin'
20
20
 
21
+ begin
22
+ require 'mongo_ext/cbson'
23
+ BSON_SERIALIZER = CBson
24
+ rescue LoadError
25
+ BSON_SERIALIZER = BSON
26
+ end
27
+
21
28
  module Mongo
22
29
  ASCENDING = 1
23
30
  DESCENDING = -1
24
31
 
25
- VERSION = "0.16"
32
+ VERSION = "0.17"
26
33
  end
@@ -202,13 +202,7 @@ module Mongo
202
202
  def insert(doc_or_docs, options={})
203
203
  doc_or_docs = [doc_or_docs] unless doc_or_docs.is_a?(Array)
204
204
  doc_or_docs.collect! { |doc| @pk_factory.create_pk(doc) }
205
- result = insert_documents(doc_or_docs)
206
- if options.delete(:safe)
207
- error = @db.error
208
- if error
209
- raise OperationFailure, error
210
- end
211
- end
205
+ result = insert_documents(doc_or_docs, @name, true, options[:safe])
212
206
  result.size > 1 ? result : result.first
213
207
  end
214
208
  alias_method :<<, :insert
@@ -227,8 +221,9 @@ module Mongo
227
221
  message.put_int(0)
228
222
  BSON.serialize_cstr(message, "#{@db.name}.#{@name}")
229
223
  message.put_int(0)
230
- message.put_array(BSON.new.serialize(selector, false).to_a)
231
- @db.send_message_with_operation(Mongo::Constants::OP_DELETE, message)
224
+ message.put_array(BSON_SERIALIZER.serialize(selector, false).unpack("C*"))
225
+ @db.send_message_with_operation(Mongo::Constants::OP_DELETE, message,
226
+ "db.#{@db.name}.remove(#{selector.inspect})")
232
227
  end
233
228
 
234
229
  # Remove all records.
@@ -240,28 +235,37 @@ module Mongo
240
235
 
241
236
  # Update a single document in this collection.
242
237
  #
243
- # :spec :: a hash specifying elements which must be present for
244
- # a document to be updated
238
+ # :selector :: a hash specifying elements which must be present for a document to be updated. Note:
239
+ # the update command currently updates only the first document matching the
240
+ # given selector. If you want all matching documents to be updated, be sure
241
+ # to specify :multi => true.
245
242
  # :document :: a hash specifying the fields to be changed in the
246
- # selected document, or (in the case of an upsert) the document to
247
- # be inserted
243
+ # selected document, or (in the case of an upsert) the document to
244
+ # be inserted
248
245
  #
249
246
  # Options:
250
247
  # :upsert :: if true, perform an upsert operation
248
+ # :multi :: update all documents matching the selector, as opposed to
249
+ # just the first matching document. Note: only works in 1.1.3 or later.
251
250
  # :safe :: if true, check that the update succeeded. OperationFailure
252
- # will be raised on an error. Checking for safety requires an extra
253
- # round-trip to the database
254
- def update(spec, document, options={})
251
+ # will be raised on an error. Checking for safety requires an extra
252
+ # round-trip to the database
253
+ def update(selector, document, options={})
255
254
  message = ByteBuffer.new
256
255
  message.put_int(0)
257
256
  BSON.serialize_cstr(message, "#{@db.name}.#{@name}")
258
- message.put_int(options[:upsert] ? 1 : 0) # 1 if a repsert operation (upsert)
259
- message.put_array(BSON.new.serialize(spec, false).to_a)
260
- message.put_array(BSON.new.serialize(document, false).to_a)
261
- @db.send_message_with_operation(Mongo::Constants::OP_UPDATE, message)
262
-
263
- if options[:safe] && error=@db.error
264
- raise OperationFailure, error
257
+ update_options = 0
258
+ update_options += 1 if options[:upsert]
259
+ update_options += 2 if options[:multi]
260
+ message.put_int(update_options)
261
+ message.put_array(BSON_SERIALIZER.serialize(selector, false).unpack("C*"))
262
+ message.put_array(BSON_SERIALIZER.serialize(document, false).unpack("C*"))
263
+ if options[:safe]
264
+ @db.send_message_with_safe_check(Mongo::Constants::OP_UPDATE, message,
265
+ "db.#{@name}.update(#{selector.inspect}, #{document.inspect})")
266
+ else
267
+ @db.send_message_with_operation(Mongo::Constants::OP_UPDATE, message,
268
+ "db.#{@name}.update(#{selector.inspect}, #{document.inspect})")
265
269
  end
266
270
  end
267
271
 
@@ -383,6 +387,28 @@ EOS
383
387
  return @db.eval(Code.new(group_function, scope))["result"]
384
388
  end
385
389
 
390
+ # Returns a list of distinct values for +key+ across all
391
+ # documents in the collection. The key may use dot notation
392
+ # to reach into an embedded object.
393
+ # @collection.save({:zip => 10010, :name => {:age => 27}})
394
+ # @collection.save({:zip => 94108, :name => {:age => 24}})
395
+ # @collection.save({:zip => 10010, :name => {:age => 27}})
396
+ # @collection.save({:zip => 99701, :name => {:age => 24}})
397
+ # @collection.save({:zip => 94108, :name => {:age => 27}})
398
+ #
399
+ # @collection.distinct(:zip)
400
+ # [10010, 94108, 99701]
401
+ # @collection.distinct("name.age")
402
+ # [27, 24]
403
+ def distinct(key)
404
+ raise MongoArgumentError unless [String, Symbol].include?(key.class)
405
+ command = OrderedHash.new
406
+ command[:distinct] = @name
407
+ command[:key] = key.to_s
408
+
409
+ @db.db_command(command)["values"]
410
+ end
411
+
386
412
  # Rename this collection.
387
413
  #
388
414
  # If operating in auth mode, client must be authorized as an admin to
@@ -456,12 +482,18 @@ EOS
456
482
  # Sends an Mongo::Constants::OP_INSERT message to the database.
457
483
  # Takes an array of +documents+, an optional +collection_name+, and a
458
484
  # +check_keys+ setting.
459
- def insert_documents(documents, collection_name=@name, check_keys=true)
485
+ def insert_documents(documents, collection_name=@name, check_keys=true, safe=false)
460
486
  message = ByteBuffer.new
461
487
  message.put_int(0)
462
488
  BSON.serialize_cstr(message, "#{@db.name}.#{collection_name}")
463
- documents.each { |doc| message.put_array(BSON.new.serialize(doc, check_keys).to_a) }
464
- @db.send_message_with_operation(Mongo::Constants::OP_INSERT, message)
489
+ documents.each { |doc| message.put_array(BSON_SERIALIZER.serialize(doc, check_keys).unpack("C*")) }
490
+ if safe
491
+ @db.send_message_with_safe_check(Mongo::Constants::OP_INSERT, message,
492
+ "db.#{collection_name}.insert(#{documents.inspect})")
493
+ else
494
+ @db.send_message_with_operation(Mongo::Constants::OP_INSERT, message,
495
+ "db.#{collection_name}.insert(#{documents.inspect})")
496
+ end
465
497
  documents.collect { |o| o[:_id] || o['_id'] }
466
498
  end
467
499
 
@@ -120,9 +120,20 @@ module Mongo
120
120
  single_db_command(name, :dropDatabase => 1)
121
121
  end
122
122
 
123
+ # Copies the database +from+ on the local server to +to+ on the specified +host+.
124
+ # +host+ defaults to 'localhost' if no value is provided.
125
+ def copy_database(from, to, host="localhost")
126
+ oh = OrderedHash.new
127
+ oh[:copydb] = 1
128
+ oh[:fromhost] = host
129
+ oh[:fromdb] = from
130
+ oh[:todb] = to
131
+ single_db_command('admin', oh)
132
+ end
133
+
123
134
  # Return the build information for the current connection.
124
135
  def server_info
125
- db("admin").db_command(:buildinfo => 1)
136
+ db("admin").command({:buildinfo => 1}, {:admin => true, :check_response => true})
126
137
  end
127
138
 
128
139
  # Returns the build version of the current server, using
@@ -20,11 +20,8 @@ module Mongo
20
20
  # A cursor over query results. Returned objects are hashes.
21
21
  class Cursor
22
22
  include Mongo::Conversions
23
-
24
23
  include Enumerable
25
24
 
26
- RESPONSE_HEADER_SIZE = 20
27
-
28
25
  attr_reader :collection, :selector, :admin, :fields,
29
26
  :order, :hint, :snapshot, :timeout,
30
27
  :full_collection_name
@@ -199,7 +196,7 @@ module Mongo
199
196
  message.put_int(0)
200
197
  message.put_int(1)
201
198
  message.put_long(@cursor_id)
202
- @db.send_message_with_operation(Mongo::Constants::OP_KILL_CURSORS, message)
199
+ @db.send_message_with_operation(Mongo::Constants::OP_KILL_CURSORS, message, "cursor.close()")
203
200
  end
204
201
  @cursor_id = 0
205
202
  @closed = true
@@ -250,14 +247,14 @@ module Mongo
250
247
  # the selector will be used in a $where clause.
251
248
  # See http://www.mongodb.org/display/DOCS/Server-side+Code+Execution
252
249
  def convert_selector_for_query(selector)
253
- case selector
254
- when Hash
255
- selector
256
- when nil
257
- {}
258
- when String
250
+ case selector
251
+ when Hash
252
+ selector
253
+ when nil
254
+ {}
255
+ when String
259
256
  {"$where" => Code.new(selector)}
260
- when Code
257
+ when Code
261
258
  {"$where" => selector}
262
259
  end
263
260
  end
@@ -267,47 +264,6 @@ module Mongo
267
264
  @order || @explain || @hint || @snapshot
268
265
  end
269
266
 
270
- def read_all
271
- read_message_header
272
- read_response_header
273
- read_objects_off_wire
274
- end
275
-
276
- def read_objects_off_wire
277
- while doc = next_object_on_wire
278
- @cache << doc
279
- end
280
- end
281
-
282
- def read_message_header
283
- message = ByteBuffer.new
284
- message.put_array(@db.receive_full(16).unpack("C*"))
285
- unless message.size == 16 #HEADER_SIZE
286
- raise "Short read for DB response header: expected #{16} bytes, saw #{message.size}"
287
- end
288
- message.rewind
289
- size = message.get_int
290
- request_id = message.get_int
291
- response_to = message.get_int
292
- op = message.get_int
293
- end
294
-
295
- def read_response_header
296
- header_buf = ByteBuffer.new
297
- header_buf.put_array(@db.receive_full(RESPONSE_HEADER_SIZE).unpack("C*"))
298
- raise "Short read for DB response header; expected #{RESPONSE_HEADER_SIZE} bytes, saw #{header_buf.length}" unless header_buf.length == RESPONSE_HEADER_SIZE
299
- header_buf.rewind
300
- @result_flags = header_buf.get_int
301
- @cursor_id = header_buf.get_long
302
- @starting_from = header_buf.get_int
303
- @n_remaining = header_buf.get_int
304
- if @n_received
305
- @n_received += @n_remaining
306
- else
307
- @n_received = @n_remaining
308
- end
309
- end
310
-
311
267
  def num_remaining
312
268
  refill_via_get_more if @cache.length == 0
313
269
  @cache.length
@@ -320,64 +276,42 @@ module Mongo
320
276
  num_remaining > 0
321
277
  end
322
278
 
323
- def next_object_on_wire
324
- # if @n_remaining is 0 but we have a non-zero cursor, there are more
325
- # to fetch, so do a GetMore operation, but don't do it here - do it
326
- # when someone pulls an object out of the cache and it's empty
327
- return nil if @n_remaining == 0
328
- object_from_stream
329
- end
330
-
331
279
  def refill_via_get_more
332
280
  return if send_query_if_needed || @cursor_id.zero?
333
- @db._synchronize {
334
- message = ByteBuffer.new
335
- # Reserved.
336
- message.put_int(0)
281
+ message = ByteBuffer.new
282
+ # Reserved.
283
+ message.put_int(0)
337
284
 
338
- # DB name.
339
- db_name = @admin ? 'admin' : @db.name
340
- BSON.serialize_cstr(message, "#{db_name}.#{@collection.name}")
285
+ # DB name.
286
+ db_name = @admin ? 'admin' : @db.name
287
+ BSON.serialize_cstr(message, "#{db_name}.#{@collection.name}")
341
288
 
342
- # Number of results to return; db decides for now.
343
- message.put_int(0)
289
+ # Number of results to return; db decides for now.
290
+ message.put_int(0)
344
291
 
345
- # Cursor id.
346
- message.put_long(@cursor_id)
347
- @db.send_message_with_operation_without_synchronize(Mongo::Constants::OP_GET_MORE, message)
348
- read_all
349
- }
292
+ # Cursor id.
293
+ message.put_long(@cursor_id)
294
+ results, @n_received, @cursor_id = @db.receive_message_with_operation(Mongo::Constants::OP_GET_MORE, message, "cursor.get_more()")
295
+ @cache += results
350
296
  close_cursor_if_query_complete
351
297
  end
352
298
 
353
- def object_from_stream
354
- buf = ByteBuffer.new
355
- buf.put_array(@db.receive_full(4).unpack("C*"))
356
- buf.rewind
357
- size = buf.get_int
358
- buf.put_array(@db.receive_full(size - 4).unpack("C*"), 4)
359
- @n_remaining -= 1
360
- buf.rewind
361
- BSON.new.deserialize(buf)
362
- end
363
-
299
+ # Run query first time we request an object from the wire
364
300
  def send_query_if_needed
365
- # Run query first time we request an object from the wire
366
301
  if @query_run
367
302
  false
368
303
  else
369
- message = construct_query_message(@query)
370
- @db._synchronize {
371
- @db.send_message_with_operation_without_synchronize(Mongo::Constants::OP_QUERY, message)
372
- @query_run = true
373
- read_all
374
- }
304
+ message = construct_query_message
305
+ results, @n_received, @cursor_id = @db.receive_message_with_operation(Mongo::Constants::OP_QUERY, message,
306
+ (query_log_message if @db.logger))
307
+ @cache += results
308
+ @query_run = true
375
309
  close_cursor_if_query_complete
376
310
  true
377
311
  end
378
312
  end
379
313
 
380
- def construct_query_message(query)
314
+ def construct_query_message
381
315
  message = ByteBuffer.new
382
316
  message.put_int(query_opts)
383
317
  db_name = @admin ? 'admin' : @db.name
@@ -388,11 +322,16 @@ module Mongo
388
322
  if query_contains_special_fields?
389
323
  selector = selector_with_special_query_fields
390
324
  end
391
- message.put_array(BSON.new.serialize(selector).to_a)
392
- message.put_array(BSON.new.serialize(@fields).to_a) if @fields
325
+ message.put_array(BSON_SERIALIZER.serialize(selector, false).unpack("C*"))
326
+ message.put_array(BSON_SERIALIZER.serialize(@fields, false).unpack("C*")) if @fields
393
327
  message
394
328
  end
395
329
 
330
+ def query_log_message
331
+ "#{@admin ? 'admin' : @db.name}.#{@collection.name}.find(#{@selector.inspect}, #{@fields ? @fields.inspect : '{}'})" +
332
+ "#{@skip != 0 ? ('.skip(' + @skip.to_s + ')') : ''}#{@limit != 0 ? ('.limit(' + @limit.to_s + ')') : ''}"
333
+ end
334
+
396
335
  def selector_with_special_query_fields
397
336
  sel = OrderedHash.new
398
337
  sel['query'] = @selector
@@ -26,6 +26,8 @@ module Mongo
26
26
  # A Mongo database.
27
27
  class DB
28
28
 
29
+ STANDARD_HEADER_SIZE = 16
30
+ RESPONSE_HEADER_SIZE = 20
29
31
  SYSTEM_NAMESPACE_COLLECTION = "system.namespaces"
30
32
  SYSTEM_INDEX_COLLECTION = "system.indexes"
31
33
  SYSTEM_PROFILE_COLLECTION = "system.profile"
@@ -49,6 +51,8 @@ module Mongo
49
51
  # The name of the database.
50
52
  attr_reader :name
51
53
 
54
+ attr_reader :connection
55
+
52
56
  # Host to which we are currently connected.
53
57
  attr_reader :host
54
58
  # Port to which we are currently connected.
@@ -133,6 +137,8 @@ module Mongo
133
137
  raise InvalidName, "database name cannot be the empty string"
134
138
  end
135
139
 
140
+ @connection = options[:connection]
141
+
136
142
  @name, @nodes = db_name, nodes
137
143
  @strict = options[:strict]
138
144
  @pk_factory = options[:pk]
@@ -431,54 +437,49 @@ module Mongo
431
437
  self.collection(collection_name).create_index(field_or_spec, unique)
432
438
  end
433
439
 
434
- def send_to_db(message)
435
- connect_to_master if !connected? && @auto_reconnect
436
- begin
437
- @logger.debug(" MONGODB #{message}") if @logger
438
- @socket.print(message.buf.to_s)
439
- @socket.flush
440
- rescue => ex
441
- close
442
- raise ex
440
+ # Sends a message to MongoDB.
441
+ #
442
+ # Takes a MongoDB opcode, +operation+, a message of class ByteBuffer,
443
+ # +message+, and an optional formatted +log_message+.
444
+ # Sends the message to the databse, adding the necessary headers.
445
+ def send_message_with_operation(operation, message, log_message=nil)
446
+ message_with_headers = add_message_headers(operation, message).to_s
447
+ @logger.debug(" MONGODB #{log_message || message}") if @logger
448
+ @semaphore.synchronize do
449
+ send_message_on_socket(message_with_headers)
443
450
  end
444
451
  end
445
452
 
446
- # Sends a message to MongoDB.
447
- #
448
- # Takes a MongoDB opcode, +operation+, and a message of class ByteBuffer,
449
- # +message+, and sends the message to the databse, adding the necessary headers.
450
- def send_message_with_operation(operation, message)
453
+ def send_message_with_operation_raw(operation, message, log_message=nil)
454
+ message_with_headers = add_message_headers_raw(operation, message)
455
+ @logger.debug(" MONGODB #{log_message || message}") if @logger
451
456
  @semaphore.synchronize do
452
- connect_to_master if !connected? && @auto_reconnect
453
- begin
454
- message_with_headers = add_message_headers(operation, message)
455
- @logger.debug(" MONGODB #{message}") if @logger
456
- @socket.print(message_with_headers.to_s)
457
- @socket.flush
458
- rescue => ex
459
- close
460
- raise ex
461
- end
457
+ send_message_on_socket(message_with_headers)
462
458
  end
463
459
  end
464
460
 
465
- def send_message_with_operation_without_synchronize(operation, message)
466
- connect_to_master if !connected? && @auto_reconnect
467
- begin
468
- message_with_headers = add_message_headers(operation, message)
469
- @logger.debug(" MONGODB #{operation} #{message}") if @logger
470
- @socket.print(message_with_headers.to_s)
471
- @socket.flush
472
- rescue => ex
473
- close
474
- raise ex
461
+ # Sends a message to the database, waits for a response, and raises
462
+ # and exception if the operation has failed.
463
+ def send_message_with_safe_check(operation, message, log_message=nil)
464
+ message_with_headers = add_message_headers(operation, message)
465
+ message_with_check = last_error_message
466
+ @logger.debug(" MONGODB #{log_message || message}") if @logger
467
+ @semaphore.synchronize do
468
+ send_message_on_socket(message_with_headers.append!(message_with_check).to_s)
469
+ docs, num_received, cursor_id = receive
470
+ if num_received == 1 && error = docs[0]['err']
471
+ raise Mongo::OperationFailure, error
472
+ end
475
473
  end
476
474
  end
477
475
 
478
- def receive_message_with_operation(operation, message)
476
+ # Send a message to the database and waits for the response.
477
+ def receive_message_with_operation(operation, message, log_message=nil)
478
+ message_with_headers = add_message_headers(operation, message).to_s
479
+ @logger.debug(" MONGODB #{log_message || message}") if @logger
479
480
  @semaphore.synchronize do
480
-
481
-
481
+ send_message_on_socket(message_with_headers)
482
+ receive
482
483
  end
483
484
  end
484
485
 
@@ -501,9 +502,34 @@ module Mongo
501
502
  cursor = Cursor.new(Collection.new(self, SYSTEM_COMMAND_COLLECTION), :admin => use_admin_db, :limit => -1, :selector => selector)
502
503
  cursor.next_object
503
504
  end
505
+
506
+ # Sends a command to the database.
507
+ #
508
+ # :selector (required) :: An OrderedHash, or a standard Hash with just one
509
+ # key, specifying the command to be performed.
510
+ #
511
+ # :admin (optional) :: If true, the command will be executed on the admin
512
+ # collection.
513
+ #
514
+ # :check_response (optional) :: If true, will raise an exception if the
515
+ # command fails.
516
+ #
517
+ # Note: DB commands must start with the "command" key. For this reason,
518
+ # any selector containing more than one key must be an OrderedHash.
519
+ def command(selector, admin=false, check_response=false)
520
+ raise MongoArgumentError, "command must be given a selector" unless selector.is_a?(Hash) && !selector.empty?
521
+ if selector.class.eql?(Hash) && selector.keys.length > 1
522
+ raise MongoArgumentError, "DB#command requires an OrderedHash when hash contains multiple keys"
523
+ end
504
524
 
505
- def _synchronize &block
506
- @semaphore.synchronize &block
525
+ result = Cursor.new(system_command_collection, :admin => admin,
526
+ :limit => -1, :selector => selector).next_object
527
+
528
+ if check_response && !ok?(result)
529
+ raise OperationFailure, "Database command '#{selector.keys.first}' failed."
530
+ else
531
+ result
532
+ end
507
533
  end
508
534
 
509
535
  def full_collection_name(collection_name)
@@ -512,6 +538,80 @@ module Mongo
512
538
 
513
539
  private
514
540
 
541
+ def receive
542
+ receive_header
543
+ number_received, cursor_id = receive_response_header
544
+ read_documents(number_received, cursor_id)
545
+ end
546
+
547
+ def receive_header
548
+ header = ByteBuffer.new
549
+ header.put_array(receive_data_on_socket(16).unpack("C*"))
550
+ unless header.size == STANDARD_HEADER_SIZE
551
+ raise "Short read for DB response header: " +
552
+ "expected #{STANDARD_HEADER_SIZE} bytes, saw #{header.size}"
553
+ end
554
+ header.rewind
555
+ size = header.get_int
556
+ request_id = header.get_int
557
+ response_to = header.get_int
558
+ op = header.get_int
559
+ end
560
+
561
+ def receive_response_header
562
+ header_buf = ByteBuffer.new
563
+ header_buf.put_array(receive_data_on_socket(RESPONSE_HEADER_SIZE).unpack("C*"))
564
+ if header_buf.length != RESPONSE_HEADER_SIZE
565
+ raise "Short read for DB response header; " +
566
+ "expected #{RESPONSE_HEADER_SIZE} bytes, saw #{header_buf.length}"
567
+ end
568
+ header_buf.rewind
569
+ result_flags = header_buf.get_int
570
+ cursor_id = header_buf.get_long
571
+ starting_from = header_buf.get_int
572
+ number_remaining = header_buf.get_int
573
+ [number_remaining, cursor_id]
574
+ end
575
+
576
+ def read_documents(number_received, cursor_id)
577
+ docs = []
578
+ number_remaining = number_received
579
+ while number_remaining > 0 do
580
+ buf = ByteBuffer.new
581
+ buf.put_array(receive_data_on_socket(4).unpack("C*"))
582
+ buf.rewind
583
+ size = buf.get_int
584
+ buf.put_array(receive_data_on_socket(size - 4).unpack("C*"), 4)
585
+ number_remaining -= 1
586
+ buf.rewind
587
+ docs << BSON.new.deserialize(buf)
588
+ end
589
+ [docs, number_received, cursor_id]
590
+ end
591
+
592
+ # Sending a message on socket.
593
+ def send_message_on_socket(packed_message)
594
+ connect_to_master if !connected? && @auto_reconnect
595
+ begin
596
+ @socket.print(packed_message)
597
+ @socket.flush
598
+ rescue => ex
599
+ close
600
+ raise ex
601
+ end
602
+ end
603
+
604
+ # Receive data of specified length on socket.
605
+ def receive_data_on_socket(length)
606
+ message = ""
607
+ while message.length < length do
608
+ chunk = @socket.recv(length - message.length)
609
+ raise "connection closed" unless chunk.length > 0
610
+ message += chunk
611
+ end
612
+ message
613
+ end
614
+
515
615
  # Prepares a message for transmission to MongoDB by
516
616
  # constructing a valid message header.
517
617
  def add_message_headers(operation, message)
@@ -538,8 +638,41 @@ module Mongo
538
638
  @@current_request_id
539
639
  end
540
640
 
641
+ # Creates a getlasterror message.
642
+ def last_error_message
643
+ generate_last_error_message
644
+ end
645
+
646
+ def generate_last_error_message
647
+ message = ByteBuffer.new
648
+ message.put_int(0)
649
+ BSON.serialize_cstr(message, "#{@name}.$cmd")
650
+ message.put_int(0)
651
+ message.put_int(-1)
652
+ message.put_array(BSON_SERIALIZER.serialize({:getlasterror => 1}, false).unpack("C*"))
653
+ add_message_headers(Mongo::Constants::OP_QUERY, message)
654
+ end
655
+
656
+ def reset_error_message
657
+ @@reset_error_message ||= generate_reset_error_message
658
+ end
659
+
660
+ def generate_reset_error_message
661
+ message = ByteBuffer.new
662
+ message.put_int(0)
663
+ BSON.serialize_cstr(message, "#{@name}.$cmd")
664
+ message.put_int(0)
665
+ message.put_int(-1)
666
+ message.put_array(BSON_SERIALIZER.serialize({:reseterror => 1}, false).unpack("C*"))
667
+ add_message_headers(Mongo::Constants::OP_QUERY, message)
668
+ end
669
+
541
670
  def hash_password(username, plaintext)
542
671
  Digest::MD5.hexdigest("#{username}:mongo:#{plaintext}")
543
672
  end
673
+
674
+ def system_command_collection
675
+ Collection.new(self, SYSTEM_COMMAND_COLLECTION)
676
+ end
544
677
  end
545
678
  end
@@ -18,14 +18,20 @@ module Mongo
18
18
  # Generic Mongo Ruby Driver exception class.
19
19
  class MongoRubyError < StandardError; end
20
20
 
21
+ # Raised when MongoDB itself has returned an error.
22
+ class MongoDBError < RuntimeError; end
23
+
21
24
  # Raised when configuration options cause connections, queries, etc., to fail.
22
25
  class ConfigurationError < MongoRubyError; end
23
26
 
27
+ # Raised when invalid arguments are sent to Mongo Ruby methods.
28
+ class MongoArgumentError < MongoRubyError; end
29
+
24
30
  # Raised when a database operation fails.
25
- class OperationFailure < RuntimeError; end
31
+ class OperationFailure < MongoDBError; end
26
32
 
27
33
  # Raised when a client attempts to perform an invalid operation.
28
- class InvalidOperation < RuntimeError; end
34
+ class InvalidOperation < MongoDBError; end
29
35
 
30
36
  # Raised when an invalid name is used.
31
37
  class InvalidName < RuntimeError; end
@@ -57,6 +57,12 @@ module Mongo
57
57
  end
58
58
  alias_method :==, :eql?
59
59
 
60
+ # Returns a unique hashcode for the object.
61
+ # This is required since we've defined an #eql? method.
62
+ def hash
63
+ @data.hash
64
+ end
65
+
60
66
  def to_a
61
67
  @data.dup
62
68
  end
@@ -120,23 +126,27 @@ module Mongo
120
126
 
121
127
  private
122
128
 
123
- def generate
124
- oid = ''
129
+ begin
130
+ require 'mongo_ext/cbson'
131
+ rescue LoadError
132
+ def generate
133
+ oid = ''
125
134
 
126
- # 4 bytes current time
127
- time = Time.new.to_i
128
- oid += [time].pack("N")
135
+ # 4 bytes current time
136
+ time = Time.new.to_i
137
+ oid += [time].pack("N")
129
138
 
130
- # 3 bytes machine
131
- oid += Digest::MD5.digest(Socket.gethostname)[0, 3]
139
+ # 3 bytes machine
140
+ oid += Digest::MD5.digest(Socket.gethostname)[0, 3]
132
141
 
133
- # 2 bytes pid
134
- oid += [Process.pid % 0xFFFF].pack("n")
142
+ # 2 bytes pid
143
+ oid += [Process.pid % 0xFFFF].pack("n")
135
144
 
136
- # 3 bytes inc
137
- oid += [get_inc].pack("N")[1, 3]
145
+ # 3 bytes inc
146
+ oid += [get_inc].pack("N")[1, 3]
138
147
 
139
- oid.unpack("C12")
148
+ oid.unpack("C12")
149
+ end
140
150
  end
141
151
 
142
152
  def get_inc
@@ -71,6 +71,12 @@ class BSON
71
71
  @buf.to_a
72
72
  end
73
73
 
74
+ # Serializes an object.
75
+ # Implemented to ensure an API compatible with BSON extension.
76
+ def self.serialize(obj, check_keys)
77
+ new.serialize(obj, check_keys)
78
+ end
79
+
74
80
  begin
75
81
  require 'mongo_ext/cbson'
76
82
  def serialize(obj, check_keys=false)
@@ -99,6 +105,12 @@ class BSON
99
105
  end
100
106
  end
101
107
 
108
+ # Returns the array stored in the buffer.
109
+ # Implemented to ensure an API compatible with BSON extension.
110
+ def unpack(arg)
111
+ @buf.to_a
112
+ end
113
+
102
114
  def serialize_key_value(k, v, check_keys)
103
115
  k = k.to_s
104
116
  if check_keys
@@ -20,9 +20,11 @@ class ByteBuffer
20
20
  attr_reader :order
21
21
 
22
22
  def initialize(initial_data=[])
23
- @buf = initial_data
24
- @cursor = 0
25
- self.order = :little_endian
23
+ @buf = initial_data
24
+ @cursor = @buf.length
25
+ @order = :little_endian
26
+ @int_pack_order = 'V'
27
+ @double_pack_order = 'E'
26
28
  end
27
29
 
28
30
  # +endianness+ should be :little_endian or :big_endian. Default is :little_endian
@@ -159,7 +161,9 @@ class ByteBuffer
159
161
  end
160
162
 
161
163
  def to_s
162
- if @buf.respond_to? "pack"
164
+ if @buf.respond_to? :fast_pack
165
+ @buf.fast_pack
166
+ elsif @buf.respond_to? "pack"
163
167
  @buf.pack("C*")
164
168
  else
165
169
  @buf
@@ -59,6 +59,21 @@ class TestCollection < Test::Unit::TestCase
59
59
  assert_equal 5, @@db.collection("test.foo").find_one()["x"]
60
60
  end
61
61
 
62
+ if @@version > "1.1"
63
+ def test_distinct
64
+ @@test.remove
65
+ @@test.insert([{:a => 0, :b => {:c => "a"}},
66
+ {:a => 1, :b => {:c => "b"}},
67
+ {:a => 1, :b => {:c => "c"}},
68
+ {:a => 2, :b => {:c => "a"}},
69
+ {:a => 3},
70
+ {:a => 3}])
71
+
72
+ assert_equal [0, 1, 2, 3], @@test.distinct(:a).sort
73
+ assert_equal ["a", "b", "c"], @@test.distinct("b.c").sort
74
+ end
75
+ end
76
+
62
77
  def test_safe_insert
63
78
  a = {"hello" => "world"}
64
79
  @@test.insert(a)
@@ -82,6 +97,20 @@ class TestCollection < Test::Unit::TestCase
82
97
  assert_equal 1, @@test.find_one(:_id => id2)["x"]
83
98
  end
84
99
 
100
+ if @@version >= "1.1.3"
101
+ def test_multi_update
102
+ @@test.save("num" => 10)
103
+ @@test.save("num" => 10)
104
+ @@test.save("num" => 10)
105
+ assert_equal 3, @@test.count
106
+
107
+ @@test.update({"num" => 10}, {"$set" => {"num" => 100}}, :multi => true)
108
+ @@test.find.each do |doc|
109
+ assert_equal 100, doc["num"]
110
+ end
111
+ end
112
+ end
113
+
85
114
  def test_upsert
86
115
  @@test.update({"page" => "/"}, {"$inc" => {"count" => 1}}, :upsert => true)
87
116
  @@test.update({"page" => "/"}, {"$inc" => {"count" => 1}}, :upsert => true)
@@ -21,8 +21,7 @@ class TestConnection < Test::Unit::TestCase
21
21
 
22
22
  def test_server_info
23
23
  server_info = @mongo.server_info
24
- assert server_info.keys.include? "version"
25
- assert server_info.keys.include? "bits"
24
+ assert server_info.keys.include?("version")
26
25
  assert_equal 1.0, server_info["ok"]
27
26
  end
28
27
 
@@ -54,6 +53,14 @@ class TestConnection < Test::Unit::TestCase
54
53
  @mongo.drop_database('ruby-mongo-info-test')
55
54
  end
56
55
 
56
+ def test_copy_database
57
+ @mongo.db('old').collection('copy-test').insert('a' => 1)
58
+ @mongo.copy_database('old', 'new')
59
+ old_object = @mongo.db('old').collection('copy-test').find.next_object
60
+ new_object = @mongo.db('new').collection('copy-test').find.next_object
61
+ assert_equal old_object, new_object
62
+ end
63
+
57
64
  def test_database_names
58
65
  @mongo.drop_database('ruby-mongo-info-test')
59
66
  @mongo.db('ruby-mongo-info-test').collection('info-test').insert('a' => 1)
@@ -73,7 +80,7 @@ class TestConnection < Test::Unit::TestCase
73
80
  logger.level = Logger::DEBUG
74
81
  db = Connection.new(@host, @port, :logger => logger).db('ruby-mongo-test')
75
82
 
76
- assert output.string.include?("2004")
83
+ assert output.string.include?("$cmd.find")
77
84
  end
78
85
 
79
86
  def test_connection_logger
@@ -189,6 +189,13 @@ class DBTest < Test::Unit::TestCase
189
189
  assert_nil @@db.previous_error
190
190
  end
191
191
 
192
+ def test_check_command_response
193
+ command = {:forceerror => 1}
194
+ assert_raise OperationFailure do
195
+ @@db.command(command, false, true)
196
+ end
197
+ end
198
+
192
199
  def test_last_status
193
200
  @@db['test'].remove
194
201
  @@db['test'].save("i" => 1)
@@ -7,7 +7,18 @@ class ObjectIDTest < Test::Unit::TestCase
7
7
  include Mongo
8
8
 
9
9
  def setup
10
- @o = ObjectID.new()
10
+ @o = ObjectID.new
11
+ end
12
+
13
+ def test_hashcode
14
+ assert_equal @o.instance_variable_get(:@data).hash, @o.hash
15
+ end
16
+
17
+ def test_array_uniq_for_equilavent_ids
18
+ a = ObjectID.new('123')
19
+ b = ObjectID.new('123')
20
+ assert_equal a, b
21
+ assert_equal 1, [a, b].uniq.size
11
22
  end
12
23
 
13
24
  def test_create_pk_method
@@ -1,18 +1,68 @@
1
- $LOAD_PATH[0,0] = File.join(File.dirname(__FILE__), '..', 'lib')
2
- require 'mongo'
3
- require 'test/unit'
1
+ require 'test/test_helper'
4
2
 
5
3
  class TestThreading < Test::Unit::TestCase
6
4
 
7
5
  include Mongo
8
6
 
9
- @@host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost'
10
- @@port = ENV['MONGO_RUBY_DRIVER_PORT'] || Connection::DEFAULT_PORT
11
- @@db = Connection.new(@@host, @@port).db('ruby-mongo-test')
7
+ @@db = Connection.new.db('ruby-mongo-test')
12
8
  @@coll = @@db.collection('thread-test-collection')
13
9
 
10
+ def set_up_safe_data
11
+ @@db.drop_collection('duplicate')
12
+ @@db.drop_collection('unique')
13
+ @duplicate = @@db.collection('duplicate')
14
+ @unique = @@db.collection('unique')
15
+
16
+ @duplicate.insert("test" => "insert")
17
+ @duplicate.insert("test" => "update")
18
+ @unique.insert("test" => "insert")
19
+ @unique.insert("test" => "update")
20
+ @unique.create_index("test", true)
21
+ end
22
+
23
+ def test_safe_update
24
+ set_up_safe_data
25
+ threads = []
26
+ 100.times do |i|
27
+ threads[i] = Thread.new do
28
+ if i % 2 == 0
29
+ assert_raise Mongo::OperationFailure do
30
+ @unique.update({"test" => "insert"}, {"$set" => {"test" => "update"}}, :safe => true)
31
+ end
32
+ else
33
+ @duplicate.update({"test" => "insert"}, {"$set" => {"test" => "update"}}, :safe => true)
34
+ end
35
+ end
36
+ end
37
+
38
+ 100.times do |i|
39
+ threads[i].join
40
+ end
41
+ end
42
+
43
+ def test_safe_insert
44
+ set_up_safe_data
45
+ threads = []
46
+ 100.times do |i|
47
+ threads[i] = Thread.new do
48
+ if i % 2 == 0
49
+ assert_raise Mongo::OperationFailure do
50
+ @unique.insert({"test" => "insert"}, :safe => true)
51
+ end
52
+ else
53
+ @duplicate.insert({"test" => "insert"}, :safe => true)
54
+ end
55
+ end
56
+ end
57
+
58
+ 100.times do |i|
59
+ threads[i].join
60
+ end
61
+ end
62
+
14
63
  def test_threading
15
- @@coll.remove
64
+ @@coll.drop
65
+ @@coll = @@db.collection('thread-test-collection')
16
66
 
17
67
  1000.times do |i|
18
68
  @@coll.insert("x" => i)
@@ -21,13 +71,13 @@ class TestThreading < Test::Unit::TestCase
21
71
  threads = []
22
72
 
23
73
  10.times do |i|
24
- threads[i] = Thread.new{
74
+ threads[i] = Thread.new do
25
75
  sum = 0
26
- @@coll.find().each { |document|
76
+ @@coll.find().each do |document|
27
77
  sum += document["x"]
28
- }
78
+ end
29
79
  assert_equal 499500, sum
30
- }
80
+ end
31
81
  end
32
82
 
33
83
  10.times do |i|
@@ -0,0 +1,54 @@
1
+ require 'test/test_helper'
2
+
3
+ class CollectionTest < Test::Unit::TestCase
4
+
5
+ class MockDB < DB
6
+ def connect_to_master
7
+ true
8
+ end
9
+ end
10
+
11
+ context "Basic operations: " do
12
+ setup do
13
+ @logger = mock()
14
+ end
15
+
16
+ should "send update message" do
17
+ @db = MockDB.new("testing", ['localhost', 27017], :logger => @logger)
18
+ @coll = @db.collection('books')
19
+ @db.expects(:send_message_with_operation).with do |op, msg, log|
20
+ op == 2001 && log.include?("db.books.update")
21
+ end
22
+ @coll.update({}, {:title => 'Moby Dick'})
23
+ end
24
+
25
+ should "send insert message" do
26
+ @db = MockDB.new("testing", ['localhost', 27017], :logger => @logger)
27
+ @coll = @db.collection('books')
28
+ @db.expects(:send_message_with_operation).with do |op, msg, log|
29
+ op == 2002 && log.include?("db.books.insert")
30
+ end
31
+ @coll.insert({:title => 'Moby Dick'})
32
+ end
33
+
34
+ should "send safe update message" do
35
+ @db = MockDB.new("testing", ['localhost', 27017], :logger => @logger)
36
+ @coll = @db.collection('books')
37
+ @db.expects(:send_message_with_safe_check).with do |op, msg, log|
38
+ op == 2001 && log.include?("db.books.update")
39
+ end
40
+ @coll.update({}, {:title => 'Moby Dick'}, :safe => true)
41
+ end
42
+
43
+ should "send safe insert message" do
44
+ @db = MockDB.new("testing", ['localhost', 27017], :logger => @logger)
45
+ @coll = @db.collection('books')
46
+ @db.expects(:send_message_with_safe_check).with do |op, msg, log|
47
+ op == 2001 && log.include?("db.books.update")
48
+ end
49
+ @coll.update({}, {:title => 'Moby Dick'}, :safe => true)
50
+ end
51
+ end
52
+ end
53
+
54
+
@@ -0,0 +1,82 @@
1
+ require 'test/test_helper'
2
+
3
+ class DBTest < Test::Unit::TestCase
4
+
5
+ class MockDB < DB
6
+ attr_accessor :socket
7
+
8
+ def connect_to_master
9
+ true
10
+ end
11
+
12
+ public :add_message_headers
13
+ end
14
+
15
+ def insert_message(db, documents)
16
+ documents = [documents] unless documents.is_a?(Array)
17
+ message = ByteBuffer.new
18
+ message.put_int(0)
19
+ BSON.serialize_cstr(message, "#{db.name}.test")
20
+ documents.each { |doc| message.put_array(BSON.new.serialize(doc, true).to_a) }
21
+ message = db.add_message_headers(Mongo::Constants::OP_INSERT, message)
22
+ end
23
+
24
+ context "DB commands" do
25
+ setup do
26
+ @db = MockDB.new("testing", ['localhost', 27017])
27
+ @collection = mock()
28
+ @db.stubs(:system_command_collection).returns(@collection)
29
+ end
30
+
31
+ should "raise an error if given a hash with more than one key" do
32
+ assert_raise MongoArgumentError do
33
+ @db.command(:buildinfo => 1, :somekey => 1)
34
+ end
35
+ end
36
+
37
+ should "raise an error if the selector is omitted" do
38
+ assert_raise MongoArgumentError do
39
+ @db.command({}, true)
40
+ end
41
+ end
42
+
43
+ should "create the proper cursor" do
44
+ @cursor = mock(:next_object => {"ok" => 1})
45
+ Cursor.expects(:new).with(@collection, :admin => true,
46
+ :limit => -1, :selector => {:buildinfo => 1}).returns(@cursor)
47
+ command = {:buildinfo => 1}
48
+ @db.command(command, true)
49
+ end
50
+
51
+ should "raise an error when the command fails" do
52
+ @cursor = mock(:next_object => {"ok" => 0})
53
+ Cursor.expects(:new).with(@collection, :admin => true,
54
+ :limit => -1, :selector => {:buildinfo => 1}).returns(@cursor)
55
+ assert_raise OperationFailure do
56
+ command = {:buildinfo => 1}
57
+ @db.command(command, true, true)
58
+ end
59
+ end
60
+ end
61
+
62
+ context "safe messages" do
63
+ setup do
64
+ @db = MockDB.new("testing", ['localhost', 27017])
65
+ @collection = mock()
66
+ @db.stubs(:system_command_collection).returns(@collection)
67
+ end
68
+
69
+ should "receive getlasterror message" do
70
+ @socket = mock()
71
+ @socket.stubs(:close)
72
+ @socket.expects(:flush)
73
+ @socket.expects(:print).with { |message| message.include?('getlasterror') }
74
+ @db.socket = @socket
75
+ @db.stubs(:receive)
76
+ message = insert_message(@db, {:a => 1})
77
+ @db.send_message_with_safe_check(Mongo::Constants::OP_QUERY, message)
78
+ end
79
+ end
80
+ end
81
+
82
+
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongo
3
3
  version: !ruby/object:Gem::Version
4
- version: "0.16"
4
+ version: "0.17"
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jim Menard
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2009-10-26 00:00:00 -04:00
13
+ date: 2009-11-16 00:00:00 -05:00
14
14
  default_executable:
15
15
  dependencies: []
16
16
 
@@ -115,4 +115,6 @@ test_files:
115
115
  - test/test_round_trip.rb
116
116
  - test/test_slave_connection.rb
117
117
  - test/test_threading.rb
118
+ - test/unit/collection_test.rb
118
119
  - test/unit/cursor_test.rb
120
+ - test/unit/db_test.rb