mongo 0.19.1 → 0.19.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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