mongo 0.19.1 → 0.19.2

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.
@@ -276,19 +276,33 @@ module Mongo
276
276
  self[name].command(:dropDatabase => 1)
277
277
  end
278
278
 
279
- # Copy the database +from+ on the local server to +to+ on the specified +host+.
280
- # +host+ defaults to 'localhost' if no value is provided.
279
+ # Copy the database +from+ to +to+ on localhost. The +from+ database is
280
+ # assumed to be on localhost, but an alternate host can be specified.
281
281
  #
282
282
  # @param [String] from name of the database to copy from.
283
283
  # @param [String] to name of the database to copy to.
284
284
  # @param [String] from_host host of the 'from' database.
285
- def copy_database(from, to, from_host="localhost")
285
+ # @param [String] username username for authentication against from_db (>=1.3.x).
286
+ # @param [String] password password for authentication against from_db (>=1.3.x).
287
+ def copy_database(from, to, from_host="localhost", username=nil, password=nil)
286
288
  oh = OrderedHash.new
287
289
  oh[:copydb] = 1
288
290
  oh[:fromhost] = from_host
289
291
  oh[:fromdb] = from
290
292
  oh[:todb] = to
291
- self["admin"].command(oh, false, true)
293
+ if username || password
294
+ unless username && password
295
+ raise MongoArgumentError, "Both username and password must be supplied for authentication."
296
+ end
297
+ nonce_cmd = OrderedHash.new
298
+ nonce_cmd[:copydbgetnonce] = 1
299
+ nonce_cmd[:fromhost] = from_host
300
+ result = self["admin"].command(nonce_cmd, true, true)
301
+ oh[:nonce] = result["nonce"]
302
+ oh[:username] = username
303
+ oh[:key] = Mongo::Support.auth_key(username, password, oh[:nonce])
304
+ end
305
+ self["admin"].command(oh, true, true)
292
306
  end
293
307
 
294
308
  # Increment and return the next available request id.
@@ -78,6 +78,13 @@ module Mongo
78
78
  doc
79
79
  end
80
80
 
81
+ # Determine whether this cursor has any remaining results.
82
+ #
83
+ # @return [Boolean]
84
+ def has_next?
85
+ num_remaining > 0
86
+ end
87
+
81
88
  # Get the size of the result set for this query.
82
89
  #
83
90
  # @return [Integer] the number of objects in the result set for this query. Does
@@ -169,7 +176,7 @@ module Mongo
169
176
  # end
170
177
  def each
171
178
  num_returned = 0
172
- while more? && (@limit <= 0 || num_returned < @limit)
179
+ while has_next? && (@limit <= 0 || num_returned < @limit)
173
180
  yield next_document
174
181
  num_returned += 1
175
182
  end
@@ -188,7 +195,7 @@ module Mongo
188
195
  raise InvalidOperation, "can't call Cursor#to_a on a used cursor" if @query_run
189
196
  rows = []
190
197
  num_returned = 0
191
- while more? && (@limit <= 0 || num_returned < @limit)
198
+ while has_next? && (@limit <= 0 || num_returned < @limit)
192
199
  rows << next_document
193
200
  num_returned += 1
194
201
  end
@@ -223,7 +230,7 @@ module Mongo
223
230
  message = ByteBuffer.new([0, 0, 0, 0])
224
231
  message.put_int(1)
225
232
  message.put_long(@cursor_id)
226
- @connection.send_message(Mongo::Constants::OP_KILL_CURSORS, message, "cursor.close()")
233
+ @connection.send_message(Mongo::Constants::OP_KILL_CURSORS, message, "cursor.close")
227
234
  end
228
235
  @cursor_id = 0
229
236
  @closed = true
@@ -305,13 +312,6 @@ module Mongo
305
312
  @cache.length
306
313
  end
307
314
 
308
- # Internal method, not for general use. Return +true+ if there are
309
- # more records to retrieve. This method does not check @limit;
310
- # Cursor#each is responsible for doing that.
311
- def more?
312
- num_remaining > 0
313
- end
314
-
315
315
  def refill_via_get_more
316
316
  return if send_initial_query || @cursor_id.zero?
317
317
  message = ByteBuffer.new([0, 0, 0, 0])
@@ -362,8 +362,9 @@ module Mongo
362
362
  end
363
363
 
364
364
  def query_log_message
365
- "#{@admin ? 'admin' : @db.name}.#{@collection.name}.find(#{@selector.inspect}, #{@fields ? @fields.inspect : '{}'})" +
366
- "#{@skip != 0 ? ('.skip(' + @skip.to_s + ')') : ''}#{@limit != 0 ? ('.limit(' + @limit.to_s + ')') : ''}"
365
+ "#{@admin ? 'admin' : @db.name}['#{@collection.name}'].find(#{@selector.inspect}, #{@fields ? @fields.inspect : '{}'})" +
366
+ "#{@skip != 0 ? ('.skip(' + @skip.to_s + ')') : ''}#{@limit != 0 ? ('.limit(' + @limit.to_s + ')') : ''}" +
367
+ "#{@order ? ('.sort(' + @order.inspect + ')') : ''}"
367
368
  end
368
369
 
369
370
  def selector_with_special_query_fields
@@ -16,7 +16,6 @@
16
16
 
17
17
  require 'socket'
18
18
  require 'timeout'
19
- require 'digest/md5'
20
19
  require 'thread'
21
20
 
22
21
  module Mongo
@@ -65,7 +64,7 @@ module Mongo
65
64
  #
66
65
  # @core databases constructor_details
67
66
  def initialize(db_name, connection, options={})
68
- @name = validate_db_name(db_name)
67
+ @name = Mongo::Support.validate_db_name(db_name)
69
68
  @connection = connection
70
69
  @strict = options[:strict]
71
70
  @pk_factory = options[:pk]
@@ -94,7 +93,7 @@ module Mongo
94
93
  auth['authenticate'] = 1
95
94
  auth['user'] = username
96
95
  auth['nonce'] = nonce
97
- auth['key'] = Digest::MD5.hexdigest("#{nonce}#{username}#{hash_password(username, password)}")
96
+ auth['key'] = Mongo::Support.auth_key(username, password, nonce)
98
97
  if ok?(command(auth))
99
98
  if save_auth
100
99
  @connection.add_auth(@name, username, password)
@@ -115,7 +114,7 @@ module Mongo
115
114
  def add_user(username, password)
116
115
  users = self[SYSTEM_USER_COLLECTION]
117
116
  user = users.find_one({:user => username}) || {:user => username}
118
- user['pwd'] = hash_password(username, password)
117
+ user['pwd'] = Mongo::Support.hash_password(username, password)
119
118
  users.save(user)
120
119
  return user
121
120
  end
@@ -379,11 +378,11 @@ module Mongo
379
378
  # @return [Hash] keys are index names and the values are lists of [key, direction] pairs
380
379
  # defining the index.
381
380
  def index_information(collection_name)
382
- sel = {:ns => full_collection_name(collection_name)}
381
+ sel = {:ns => full_collection_name(collection_name)}
383
382
  info = {}
384
- Cursor.new(Collection.new(self, SYSTEM_INDEX_COLLECTION), :selector => sel).each { |index|
385
- info[index['name']] = index['key'].map {|k| k}
386
- }
383
+ Cursor.new(Collection.new(self, SYSTEM_INDEX_COLLECTION), :selector => sel).each do |index|
384
+ info[index['name']] = index
385
+ end
387
386
  info
388
387
  end
389
388
 
@@ -416,8 +415,9 @@ module Mongo
416
415
  # Note: DB commands must start with the "command" key. For this reason,
417
416
  # any selector containing more than one key must be an OrderedHash.
418
417
  #
419
- # It may be of interest hat a command in MongoDB is technically a kind of query
420
- # that occurs on the system command collection ($cmd).
418
+ # Note also that a command in MongoDB is just a kind of query
419
+ # that occurs on the system command collection ($cmd). Examine this method's implementation
420
+ # to see how it works.
421
421
  #
422
422
  # @param [OrderedHash, Hash] selector an OrderedHash, or a standard Hash with just one
423
423
  # key, specifying the command to be performed.
@@ -443,7 +443,7 @@ module Mongo
443
443
  :limit => -1, :selector => selector, :socket => sock).next_document
444
444
 
445
445
  if check_response && !ok?(result)
446
- raise OperationFailure, "Database command '#{selector.keys.first}' failed."
446
+ raise OperationFailure, "Database command '#{selector.keys.first}' failed: #{result.inspect}"
447
447
  else
448
448
  result
449
449
  end
@@ -545,26 +545,8 @@ module Mongo
545
545
 
546
546
  private
547
547
 
548
- def hash_password(username, plaintext)
549
- Digest::MD5.hexdigest("#{username}:mongo:#{plaintext}")
550
- end
551
-
552
548
  def system_command_collection
553
549
  Collection.new(self, SYSTEM_COMMAND_COLLECTION)
554
550
  end
555
-
556
- def validate_db_name(db_name)
557
- unless [String, Symbol].include?(db_name.class)
558
- raise TypeError, "db_name must be a string or symbol"
559
- end
560
-
561
- [" ", ".", "$", "/", "\\"].each do |invalid_char|
562
- if db_name.include? invalid_char
563
- raise InvalidName, "database names cannot contain the character '#{invalid_char}'"
564
- end
565
- end
566
- raise InvalidName, "database name cannot be the empty string" if db_name.empty?
567
- db_name
568
- end
569
551
  end
570
552
  end
@@ -78,7 +78,7 @@ module Mongo
78
78
  # @return [Boolean]
79
79
  def delete(id)
80
80
  @files.remove({"_id" => id})
81
- @chunks.remove({"_id" => id})
81
+ @chunks.remove({"files_id" => id})
82
82
  end
83
83
 
84
84
  private
@@ -78,6 +78,7 @@ module Mongo
78
78
  # @return [String]
79
79
  # the data in the file
80
80
  def read(length=nil)
81
+ return '' if @file_length.zero?
81
82
  if length == 0
82
83
  return ''
83
84
  elsif length.nil? && @file_position.zero?
@@ -165,6 +166,9 @@ module Mongo
165
166
  # @return [True]
166
167
  def close
167
168
  if @mode[0] == ?w
169
+ if @current_chunk['n'].zero? && @chunk_position.zero?
170
+ warn "Warning: Storing a file with zero length."
171
+ end
168
172
  @upload_date = Time.now.utc
169
173
  @files.insert(to_mongo_object)
170
174
  end
@@ -0,0 +1,37 @@
1
+ # --
2
+ # Copyright (C) 2008-2010 10gen Inc.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ # ++
16
+
17
+ #:nodoc:
18
+ class Object
19
+
20
+ #:nodoc:
21
+ def returning(value)
22
+ yield value
23
+ value
24
+ end
25
+
26
+ end
27
+
28
+ #:nodoc:
29
+ class Hash
30
+
31
+ #:nodoc:
32
+ def assert_valid_keys(*valid_keys)
33
+ unknown_keys = keys - [valid_keys].flatten
34
+ raise(Mongo::MongoArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
35
+ end
36
+
37
+ end
@@ -14,13 +14,46 @@
14
14
  # limitations under the License.
15
15
  # ++
16
16
 
17
- #:nodoc:
18
- class Object
17
+ require 'digest/md5'
19
18
 
20
- #:nodoc:
21
- def returning(value)
22
- yield value
23
- value
24
- end
19
+ module Mongo
20
+ module Support
21
+ extend self
22
+
23
+ # Generate an MD5 for authentication.
24
+ #
25
+ # @param [String] username
26
+ # @param [String] password
27
+ # @param [String] nonce
28
+ #
29
+ # @return [String] a key for db authentication.
30
+ def auth_key(username, password, nonce)
31
+ Digest::MD5.hexdigest("#{nonce}#{username}#{hash_password(username, password)}")
32
+ end
33
+
34
+ # Return a hashed password for auth.
35
+ #
36
+ # @param [String] username
37
+ # @param [String] plaintext
38
+ #
39
+ # @return [String]
40
+ def hash_password(username, plaintext)
41
+ Digest::MD5.hexdigest("#{username}:mongo:#{plaintext}")
42
+ end
25
43
 
44
+
45
+ def validate_db_name(db_name)
46
+ unless [String, Symbol].include?(db_name.class)
47
+ raise TypeError, "db_name must be a string or symbol"
48
+ end
49
+
50
+ [" ", ".", "$", "/", "\\"].each do |invalid_char|
51
+ if db_name.include? invalid_char
52
+ raise InvalidName, "database names cannot contain the character '#{invalid_char}'"
53
+ end
54
+ end
55
+ raise InvalidName, "database name cannot be the empty string" if db_name.empty?
56
+ db_name
57
+ end
58
+ end
26
59
  end
@@ -0,0 +1,166 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ require 'mongo'
3
+ require 'test/unit'
4
+ require 'test/test_helper'
5
+
6
+ # Demonstrate features in MongoDB 1.4
7
+ class Features14Test < Test::Unit::TestCase
8
+
9
+ context "MongoDB 1.4" do
10
+ setup do
11
+ @con = Mongo::Connection.new
12
+ @db = @con['mongo-ruby-test']
13
+ @col = @db['new-features']
14
+ end
15
+
16
+ teardown do
17
+ @col.drop
18
+ end
19
+
20
+ context "new query operators: " do
21
+
22
+ context "$elemMatch: " do
23
+ setup do
24
+ @col.save({:user => 'bob', :updates => [{:date => Time.now.utc, :body => 'skiing', :n => 1},
25
+ {:date => Time.now.utc, :body => 'biking', :n => 2}]})
26
+
27
+ @col.save({:user => 'joe', :updates => [{:date => Time.now.utc, :body => 'skiing', :n => 2},
28
+ {:date => Time.now.utc, :body => 'biking', :n => 10}]})
29
+ end
30
+
31
+ should "match a document with a matching object element in an array" do
32
+ doc = @col.find_one({"updates" => {"$elemMatch" => {"body" => "skiing", "n" => 2}}})
33
+ assert_equal 'joe', doc['user']
34
+ end
35
+
36
+ should "$elemMatch with a conditional operator" do
37
+ doc1 = @col.find_one({"updates" => {"$elemMatch" => {"body" => "biking", "n" => {"$gt" => 5}}}})
38
+ assert_equal 'joe', doc1['user']
39
+ end
40
+
41
+ should "note the difference between $elemMatch and a traditional match" do
42
+ doc = @col.find({"updates.body" => "skiing", "updates.n" => 2}).to_a
43
+ assert_equal 2, doc.size
44
+ end
45
+ end
46
+
47
+ context "$all with regexes" do
48
+ setup do
49
+ @col.save({:n => 1, :a => 'whale'})
50
+ @col.save({:n => 2, :a => 'snake'})
51
+ end
52
+
53
+ should "match multiple regexes" do
54
+ doc = @col.find({:a => {'$all' => [/ha/, /le/]}}).to_a
55
+ assert_equal 1, doc.size
56
+ assert_equal 1, doc.first['n']
57
+ end
58
+
59
+ should "not match if not every regex matches" do
60
+ doc = @col.find({:a => {'$all' => [/ha/, /sn/]}}).to_a
61
+ assert_equal 0, doc.size
62
+ end
63
+ end
64
+
65
+ context "the $not operator" do
66
+ setup do
67
+ @col.save({:a => ['x']})
68
+ @col.save({:a => ['x', 'y']})
69
+ @col.save({:a => ['x', 'y', 'z']})
70
+ end
71
+
72
+ should "negate a standard operator" do
73
+ results = @col.find({:a => {'$not' => {'$size' => 2}}}).to_a
74
+ assert_equal 2, results.size
75
+ results = results.map {|r| r['a']}
76
+ assert_equal ['x'], results.sort.first
77
+ assert_equal ['x', 'y', 'z'], results.sort.last
78
+ end
79
+ end
80
+ end
81
+
82
+ context "new update operators: " do
83
+
84
+ context "$addToSet (pushing a unique value)" do
85
+ setup do
86
+ @col.save({:username => 'bob', :interests => ['skiing', 'guitar']})
87
+ end
88
+
89
+ should "add an item to a set uniquely ($addToSet)" do
90
+ @col.update({:username => 'bob'}, {'$addToSet' => {'interests' => 'skiing'}})
91
+ @col.update({:username => 'bob'}, {'$addToSet' => {'interests' => 'kayaking'}})
92
+ document = @col.find_one({:username => 'bob'})
93
+ assert_equal ['guitar', 'kayaking', 'skiing'], document['interests'].sort
94
+ end
95
+
96
+ should "add an array of items uniquely ($addToSet with $each)" do
97
+ @col.update({:username => 'bob'}, {'$addToSet' => {'interests' => {'$each' => ['skiing', 'kayaking', 'biking']}}})
98
+ document = @col.find_one({:username => 'bob'})
99
+ assert_equal ['biking', 'guitar', 'kayaking', 'skiing'], document['interests'].sort
100
+ end
101
+ end
102
+
103
+ context "the positional operator ($)" do
104
+ setup do
105
+ @id1 = @col.insert({:text => 'hello',
106
+ :comments => [{'by' => 'bob',
107
+ 'text' => 'lol!'},
108
+ {'by' => 'susie',
109
+ 'text' => 'bye bye!'}]})
110
+ @id2 = @col.insert({:text => 'goodbye',
111
+ :comments => [{'by' => 'bob',
112
+ 'text' => 'au revoir'},
113
+ {'by' => 'susie',
114
+ 'text' => 'bye bye!'}]})
115
+ end
116
+
117
+ should "update a matching array item" do
118
+ @col.update({"_id" => @id1, "comments.by" => 'bob'}, {'$set' => {'comments.$.text' => 'lmao!'}}, :multi => true)
119
+ result = @col.find_one({"_id" => @id1})
120
+ assert_equal 'lmao!', result['comments'][0]['text']
121
+ end
122
+ end
123
+ end
124
+
125
+ context "Geoindexing" do
126
+ setup do
127
+ @places = @db['places']
128
+ @places.create_index([['loc', Mongo::GEO2D]])
129
+
130
+ @empire_state = ([40.748371, -73.985031])
131
+ @jfk = ([40.643711, -73.790009])
132
+
133
+ @places.insert({'name' => 'Empire State Building', 'loc' => ([40.748371, -73.985031])})
134
+ @places.insert({'name' => 'Flatiron Building', 'loc' => ([40.741581, -73.987549])})
135
+ @places.insert({'name' => 'Grand Central', 'loc' => ([40.751678, -73.976562])})
136
+ @places.insert({'name' => 'Columbia University', 'loc' => ([40.808922, -73.961617])})
137
+ @places.insert({'name' => 'NYSE', 'loc' => ([40.71455, -74.007124])})
138
+ @places.insert({'name' => 'JFK', 'loc' => ([40.643711, -73.790009])})
139
+ end
140
+
141
+ teardown do
142
+ @places.drop
143
+ end
144
+
145
+ should "find the nearest addresses" do
146
+ results = @places.find({'loc' => {'$near' => @empire_state}}).limit(2).to_a
147
+ assert_equal 2, results.size
148
+ assert_equal 'Empire State Building', results[0]['name']
149
+ assert_equal 'Flatiron Building', results[1]['name']
150
+ end
151
+
152
+ should "use geoNear command to return distances from a point" do
153
+ cmd = OrderedHash.new
154
+ cmd['geoNear'] = 'places'
155
+ cmd['near'] = @empire_state
156
+ cmd['num'] = 6
157
+ r = @db.command(cmd)
158
+
159
+ assert_equal 6, r['results'].length
160
+ r['results'].each do |result|
161
+ puts result.inspect
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end