mongo 0.1.0 → 0.15

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. data/README.rdoc +268 -71
  2. data/Rakefile +27 -62
  3. data/bin/bson_benchmark.rb +59 -0
  4. data/bin/mongo_console +3 -3
  5. data/bin/run_test_script +19 -0
  6. data/bin/standard_benchmark +109 -0
  7. data/examples/admin.rb +41 -0
  8. data/examples/benchmarks.rb +42 -0
  9. data/examples/blog.rb +76 -0
  10. data/examples/capped.rb +23 -0
  11. data/examples/cursor.rb +47 -0
  12. data/examples/gridfs.rb +87 -0
  13. data/examples/index_test.rb +125 -0
  14. data/examples/info.rb +30 -0
  15. data/examples/queries.rb +69 -0
  16. data/examples/simple.rb +23 -0
  17. data/examples/strict.rb +34 -0
  18. data/examples/types.rb +35 -0
  19. data/lib/mongo.rb +9 -2
  20. data/lib/mongo/admin.rb +65 -68
  21. data/lib/mongo/collection.rb +379 -117
  22. data/lib/mongo/connection.rb +151 -0
  23. data/lib/mongo/cursor.rb +271 -216
  24. data/lib/mongo/db.rb +500 -315
  25. data/lib/mongo/errors.rb +26 -0
  26. data/lib/mongo/gridfs.rb +16 -0
  27. data/lib/mongo/gridfs/chunk.rb +92 -0
  28. data/lib/mongo/gridfs/grid_store.rb +464 -0
  29. data/lib/mongo/message.rb +16 -0
  30. data/lib/mongo/message/get_more_message.rb +24 -13
  31. data/lib/mongo/message/insert_message.rb +29 -11
  32. data/lib/mongo/message/kill_cursors_message.rb +23 -12
  33. data/lib/mongo/message/message.rb +74 -62
  34. data/lib/mongo/message/message_header.rb +35 -24
  35. data/lib/mongo/message/msg_message.rb +21 -9
  36. data/lib/mongo/message/opcodes.rb +26 -15
  37. data/lib/mongo/message/query_message.rb +63 -43
  38. data/lib/mongo/message/remove_message.rb +29 -12
  39. data/lib/mongo/message/update_message.rb +30 -13
  40. data/lib/mongo/query.rb +97 -89
  41. data/lib/mongo/types/binary.rb +25 -21
  42. data/lib/mongo/types/code.rb +30 -0
  43. data/lib/mongo/types/dbref.rb +19 -23
  44. data/lib/mongo/types/objectid.rb +130 -116
  45. data/lib/mongo/types/regexp_of_holding.rb +27 -31
  46. data/lib/mongo/util/bson.rb +273 -160
  47. data/lib/mongo/util/byte_buffer.rb +32 -28
  48. data/lib/mongo/util/ordered_hash.rb +88 -42
  49. data/lib/mongo/util/xml_to_ruby.rb +18 -15
  50. data/mongo-ruby-driver.gemspec +103 -0
  51. data/test/mongo-qa/_common.rb +8 -0
  52. data/test/mongo-qa/admin +26 -0
  53. data/test/mongo-qa/capped +22 -0
  54. data/test/mongo-qa/count1 +18 -0
  55. data/test/mongo-qa/dbs +22 -0
  56. data/test/mongo-qa/find +10 -0
  57. data/test/mongo-qa/find1 +15 -0
  58. data/test/mongo-qa/gridfs_in +16 -0
  59. data/test/mongo-qa/gridfs_out +17 -0
  60. data/test/mongo-qa/indices +49 -0
  61. data/test/mongo-qa/remove +25 -0
  62. data/test/mongo-qa/stress1 +35 -0
  63. data/test/mongo-qa/test1 +11 -0
  64. data/test/mongo-qa/update +18 -0
  65. data/{tests → test}/test_admin.rb +25 -16
  66. data/test/test_bson.rb +268 -0
  67. data/{tests → test}/test_byte_buffer.rb +0 -0
  68. data/test/test_chunk.rb +84 -0
  69. data/test/test_collection.rb +282 -0
  70. data/test/test_connection.rb +101 -0
  71. data/test/test_cursor.rb +321 -0
  72. data/test/test_db.rb +196 -0
  73. data/test/test_db_api.rb +798 -0
  74. data/{tests → test}/test_db_connection.rb +4 -3
  75. data/test/test_grid_store.rb +284 -0
  76. data/{tests → test}/test_message.rb +1 -1
  77. data/test/test_objectid.rb +105 -0
  78. data/{tests → test}/test_ordered_hash.rb +55 -0
  79. data/{tests → test}/test_round_trip.rb +13 -9
  80. data/test/test_threading.rb +37 -0
  81. metadata +74 -32
  82. data/bin/validate +0 -51
  83. data/lib/mongo/mongo.rb +0 -74
  84. data/lib/mongo/types/undefined.rb +0 -31
  85. data/tests/test_bson.rb +0 -135
  86. data/tests/test_cursor.rb +0 -66
  87. data/tests/test_db.rb +0 -51
  88. data/tests/test_db_api.rb +0 -349
  89. data/tests/test_objectid.rb +0 -88
@@ -1,153 +1,415 @@
1
1
  # --
2
2
  # Copyright (C) 2008-2009 10gen Inc.
3
3
  #
4
- # This program is free software: you can redistribute it and/or modify it
5
- # under the terms of the GNU Affero General Public License, version 3, as
6
- # published by the Free Software Foundation.
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
7
  #
8
- # This program is distributed in the hope that it will be useful, but WITHOUT
9
- # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
10
- # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License
11
- # for more details.
8
+ # http://www.apache.org/licenses/LICENSE-2.0
12
9
  #
13
- # You should have received a copy of the GNU Affero General Public License
14
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
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
15
  # ++
16
16
 
17
17
  require 'mongo/query'
18
18
 
19
- module XGen
20
- module Mongo
21
- module Driver
19
+ module Mongo
22
20
 
23
- # A named collection of records in a database.
24
- class Collection
21
+ # A named collection of records in a database.
22
+ class Collection
25
23
 
26
- attr_reader :db, :name, :hint_fields
24
+ attr_reader :db, :name, :hint
27
25
 
28
- def initialize(db, name)
29
- @db = db
30
- @name = name
31
- end
26
+ def initialize(db, name)
27
+ case name
28
+ when Symbol, String
29
+ else
30
+ raise TypeError, "new_name must be a string or symbol"
31
+ end
32
32
 
33
- # Set hint fields to use and return +self+. hint_fields may be a
34
- # single field name, array of field names, or a hash whose keys will
35
- # become the hint field names. May be +nil+.
36
- def hint(hint_fields)
37
- @hint_fields = case hint_fields
38
- when String
39
- [hint_fields]
40
- when Hash
41
- hint_fields.keys
42
- when nil
43
- nil
44
- else
45
- hint_fields.to_a
46
- end
47
- self
48
- end
33
+ name = name.to_s
49
34
 
50
- # Return records that match a +selector+ hash. See Mongo docs for
51
- # details.
52
- #
53
- # Options:
54
- # :fields :: Array of collection field names; only those will be returned (plus _id if defined)
55
- # :offset :: Start at this record when returning records
56
- # :limit :: Maximum number of records to return
57
- # :sort :: Either hash of field names as keys and 1/-1 as values; 1 ==
58
- # ascending, -1 == descending, or array of field names (all
59
- # assumed to be sorted in ascending order).
60
- def find(selector={}, options={})
61
- fields = options.delete(:fields)
62
- fields = nil if fields && fields.empty?
63
- offset = options.delete(:offset) || 0
64
- limit = options.delete(:limit) || 0
65
- sort = options.delete(:sort)
66
- raise RuntimeError, "Unknown options [#{options.inspect}]" unless options.empty?
67
- @db.query(self, Query.new(selector, fields, offset, limit, sort))
68
- end
35
+ if name.empty? or name.include? ".."
36
+ raise InvalidName, "collection names cannot be empty"
37
+ end
38
+ if name.include? "$" and not name.match(/^\$cmd/)
39
+ raise InvalidName, "collection names must not contain '$'"
40
+ end
41
+ if name.match(/^\./) or name.match(/\.$/)
42
+ raise InvalidName, "collection names must not start or end with '.'"
43
+ end
69
44
 
70
- # Insert +objects+, which are hashes. "<<" is aliased to this method.
71
- def insert(*objects)
72
- objects = objects.first if objects.size == 1 && objects.first.is_a?(Array)
73
- res = @db.insert_into_db(@name, objects)
74
- res.size > 1 ? res : res.first
75
- end
76
- alias_method :<<, :insert
45
+ @db, @name = db, name
46
+ @hint = nil
47
+ end
77
48
 
78
- # Remove the records that match +selector+.
79
- def remove(selector={})
80
- @db.remove_from_db(@name, selector)
81
- end
49
+ # Get a sub-collection of this collection by name.
50
+ #
51
+ # Raises InvalidName if an invalid collection name is used.
52
+ #
53
+ # :name :: the name of the collection to get
54
+ def [](name)
55
+ name = "#{self.name}.#{name}"
56
+ return Collection.new(db, name) if !db.strict? || db.collection_names.include?(name)
57
+ raise "Collection #{name} doesn't exist. Currently in strict mode."
58
+ end
82
59
 
83
- # Remove all records.
84
- def clear
85
- remove({})
86
- end
60
+ # Set hint fields to use and return +self+. hint may be a single field
61
+ # name, array of field names, or a hash (preferably an OrderedHash).
62
+ # May be +nil+.
63
+ def hint=(hint)
64
+ @hint = normalize_hint_fields(hint)
65
+ self
66
+ end
87
67
 
88
- # Update records that match +selector+ by applying +obj+ as an update.
89
- # If no match, inserts (???).
90
- def repsert(selector, obj)
91
- @db.repsert_in_db(@name, selector, obj)
92
- end
68
+ # Query the database.
69
+ #
70
+ # The +selector+ argument is a prototype document that all results must
71
+ # match. For example:
72
+ #
73
+ # collection.find({"hello" => "world"})
74
+ #
75
+ # only matches documents that have a key "hello" with value "world".
76
+ # Matches can have other keys *in addition* to "hello".
77
+ #
78
+ # If given an optional block +find+ will yield a Cursor to that block,
79
+ # close the cursor, and then return nil. This guarantees that partially
80
+ # evaluated cursors will be closed. If given no block +find+ returns a
81
+ # cursor.
82
+ #
83
+ # :selector :: A document (hash) specifying elements which must be
84
+ # present for a document to be included in the result set.
85
+ #
86
+ # Options:
87
+ # :fields :: Array of field names that should be returned in the result
88
+ # set ("_id" will always be included). By limiting results
89
+ # to a certain subset of fields you can cut down on network
90
+ # traffic and decoding time.
91
+ # :skip :: Number of documents to omit (from the start of the result set)
92
+ # when returning the results
93
+ # :limit :: Maximum number of records to return
94
+ # :sort :: Either hash of field names as keys and 1/-1 as values; 1 ==
95
+ # ascending, -1 == descending, or array of field names (all
96
+ # assumed to be sorted in ascending order).
97
+ # :hint :: See #hint. This option overrides the collection-wide value.
98
+ # :snapshot :: If true, snapshot mode will be used for this query.
99
+ # Snapshot mode assures no duplicates are returned, or
100
+ # objects missed, which were preset at both the start and
101
+ # end of the query's execution. For details see
102
+ # http://www.mongodb.org/display/DOCS/How+to+do+Snapshotting+in+the+Mongo+Database
103
+ def find(selector={}, options={})
104
+ fields = options.delete(:fields)
105
+ fields = ["_id"] if fields && fields.empty?
106
+ skip = options.delete(:offset) || nil
107
+ if !skip.nil?
108
+ warn "the :offset option to find is deprecated and will be removed. please use :skip instead"
109
+ end
110
+ skip = options.delete(:skip) || skip || 0
111
+ limit = options.delete(:limit) || 0
112
+ sort = options.delete(:sort)
113
+ hint = options.delete(:hint)
114
+ snapshot = options.delete(:snapshot)
115
+ if hint
116
+ hint = normalize_hint_fields(hint)
117
+ else
118
+ hint = @hint # assumed to be normalized already
119
+ end
120
+ raise RuntimeError, "Unknown options [#{options.inspect}]" unless options.empty?
93
121
 
94
- # Update records that match +selector+ by applying +obj+ as an update.
95
- def replace(selector, obj)
96
- @db.replace_in_db(@name, selector, obj)
97
- end
122
+ cursor = @db.query(self, Query.new(selector, fields, skip, limit, sort, hint, snapshot))
123
+ if block_given?
124
+ yield cursor
125
+ cursor.close()
126
+ nil
127
+ else
128
+ cursor
129
+ end
130
+ end
98
131
 
99
- # Update records that match +selector+ by applying +obj+ as an update.
100
- # Both +selector+ and +modifier_obj+ are required.
101
- def modify(selector, modifier_obj)
102
- raise "no object" unless modifier_obj
103
- raise "no selector" unless selector
104
- @db.modify_in_db(@name, selector, modifier_obj)
105
- end
132
+ # Get a single object from the database.
133
+ #
134
+ # Raises TypeError if the argument is of an improper type. Returns a
135
+ # single document (hash), or nil if no result is found.
136
+ #
137
+ # :spec_or_object_id :: a hash specifying elements which must be
138
+ # present for a document to be included in the result set OR an
139
+ # instance of ObjectID to be used as the value for an _id query.
140
+ # if nil an empty spec, {}, will be used.
141
+ # :options :: options, as passed to Collection#find
142
+ def find_one(spec_or_object_id=nil, options={})
143
+ spec = case spec_or_object_id
144
+ when nil
145
+ {}
146
+ when ObjectID
147
+ {:_id => spec_or_object_id}
148
+ when Hash
149
+ spec_or_object_id
150
+ else
151
+ raise TypeError, "spec_or_object_id must be an instance of ObjectID or Hash, or nil"
152
+ end
153
+ find(spec, options.merge(:limit => -1)).next_object
154
+ end
106
155
 
107
- # Create a new index named +index_name+. +fields+ should be an array
108
- # of field names.
109
- def create_index(name, *fields)
110
- @db.create_index(@name, name, fields)
111
- end
156
+ # Save a document in this collection.
157
+ #
158
+ # If +to_save+ already has an '_id' then an update (upsert) operation
159
+ # is performed and any existing document with that _id is overwritten.
160
+ # Otherwise an insert operation is performed. Returns the _id of the
161
+ # saved document.
162
+ #
163
+ # :to_save :: the document (a hash) to be saved
164
+ #
165
+ # Options:
166
+ # :safe :: if true, check that the save succeeded. OperationFailure
167
+ # will be raised on an error. Checking for safety requires an extra
168
+ # round-trip to the database
169
+ def save(to_save, options={})
170
+ if id = to_save[:_id] || to_save['_id']
171
+ update({:_id => id}, to_save, :upsert => true, :safe => options.delete(:safe))
172
+ id
173
+ else
174
+ insert(to_save, :safe => options.delete(:safe))
175
+ end
176
+ end
112
177
 
113
- # Drop index +name+.
114
- def drop_index(name)
115
- @db.drop_index(@name, name)
178
+ # Insert a document(s) into this collection.
179
+ #
180
+ # "<<" is aliased to this method. Returns the _id of the inserted
181
+ # document or a list of _ids of the inserted documents. The object(s)
182
+ # may have been modified by the database's PK factory, if it has one.
183
+ #
184
+ # :doc_or_docs :: a document (as a hash) or Array of documents to be
185
+ # inserted
186
+ #
187
+ # Options:
188
+ # :safe :: if true, check that the insert succeeded. OperationFailure
189
+ # will be raised on an error. Checking for safety requires an extra
190
+ # round-trip to the database
191
+ def insert(doc_or_docs, options={})
192
+ doc_or_docs = [doc_or_docs] if !doc_or_docs.is_a?(Array)
193
+ res = @db.insert_into_db(@name, doc_or_docs)
194
+ if options.delete(:safe)
195
+ error = @db.error
196
+ if error
197
+ raise OperationFailure, error
116
198
  end
199
+ end
200
+ res.size > 1 ? res : res.first
201
+ end
202
+ alias_method :<<, :insert
203
+
204
+ # Remove the records that match +selector+.
205
+ def remove(selector={})
206
+ @db.remove_from_db(@name, selector)
207
+ end
208
+
209
+ # Remove all records.
210
+ def clear
211
+ remove({})
212
+ end
213
+
214
+ # Update a single document in this collection.
215
+ #
216
+ # :spec :: a hash specifying elements which must be present for
217
+ # a document to be updated
218
+ # :document :: a hash specifying the fields to be changed in the
219
+ # selected document, or (in the case of an upsert) the document to
220
+ # be inserted
221
+ #
222
+ # Options:
223
+ # :upsert :: if true, perform an upsert operation
224
+ # :safe :: if true, check that the update succeeded. OperationFailure
225
+ # will be raised on an error. Checking for safety requires an extra
226
+ # round-trip to the database
227
+ def update(spec, document, options={})
228
+ upsert = options.delete(:upsert)
229
+ safe = options.delete(:safe)
117
230
 
118
- # Drop all indexes.
119
- def drop_indexes
120
- # just need to call drop indexes with no args; will drop them all
121
- @db.drop_index(@name, '*')
231
+ if upsert
232
+ @db.repsert_in_db(@name, spec, document)
233
+ else
234
+ @db.replace_in_db(@name, spec, document)
235
+ end
236
+ if safe
237
+ error = @db.error
238
+ if error
239
+ raise OperationFailure, error
122
240
  end
241
+ end
242
+ end
243
+
244
+ # Create a new index. +field_or_spec+
245
+ # should be either a single field name or a Array of [field name,
246
+ # direction] pairs. Directions should be specified as
247
+ # Mongo::ASCENDING or Mongo::DESCENDING.
248
+ # +unique+ is an optional boolean indicating whether this index
249
+ # should enforce a uniqueness constraint.
250
+ def create_index(field_or_spec, unique=false)
251
+ @db.create_index(@name, field_or_spec, unique)
252
+ end
253
+
254
+ # Drop index +name+.
255
+ def drop_index(name)
256
+ @db.drop_index(@name, name)
257
+ end
258
+
259
+ # Drop all indexes.
260
+ def drop_indexes
261
+ # just need to call drop indexes with no args; will drop them all
262
+ @db.drop_index(@name, '*')
263
+ end
264
+
265
+ # Drop the entire collection. USE WITH CAUTION.
266
+ def drop
267
+ @db.drop_collection(@name)
268
+ end
123
269
 
124
- # Return an array of hashes, one for each index. Each hash contains:
125
- #
126
- # :name :: Index name
127
- #
128
- # :keys :: Hash whose keys are the names of the fields that make up
129
- # the key and values are integers.
130
- #
131
- # :ns :: Namespace; same as this collection's name.
132
- def index_information
133
- @db.index_information(@name)
270
+ # Perform a query similar to an SQL group by operation.
271
+ #
272
+ # Returns an array of grouped items.
273
+ #
274
+ # :keys :: Array of fields to group by
275
+ # :condition :: specification of rows to be considered (as a 'find'
276
+ # query specification)
277
+ # :initial :: initial value of the aggregation counter object
278
+ # :reduce :: aggregation function as a JavaScript string
279
+ # :command :: if true, run the group as a command instead of in an
280
+ # eval - it is likely that this option will eventually be
281
+ # deprecated and all groups will be run as commands
282
+ def group(keys, condition, initial, reduce, command=false)
283
+ if command
284
+ hash = {}
285
+ keys.each do |k|
286
+ hash[k] = 1
134
287
  end
135
288
 
136
- # Return a hash containing options that apply to this collection.
137
- # 'create' will be the collection name. For the other possible keys
138
- # and values, see DB#create_collection.
139
- def options
140
- @db.collections_info(@name).next_object()['options']
289
+ case reduce
290
+ when Code
291
+ else
292
+ reduce = Code.new(reduce)
141
293
  end
142
294
 
143
- # Return the number of records that match +selector+. If +selector+ is
144
- # +nil+ or an empty hash, returns the count of all records.
145
- def count(selector={})
146
- @db.count(@name, selector || {})
295
+ result = @db.db_command({"group" =>
296
+ {
297
+ "ns" => @name,
298
+ "$reduce" => reduce,
299
+ "key" => hash,
300
+ "cond" => condition,
301
+ "initial" => initial}})
302
+ if result["ok"] == 1
303
+ return result["retval"]
304
+ else
305
+ raise OperationFailure, "group command failed: #{result['errmsg']}"
147
306
  end
307
+ end
148
308
 
309
+ case reduce
310
+ when Code
311
+ scope = reduce.scope
312
+ else
313
+ scope = {}
314
+ end
315
+ scope.merge!({
316
+ "ns" => @name,
317
+ "keys" => keys,
318
+ "condition" => condition,
319
+ "initial" => initial })
320
+
321
+ group_function = <<EOS
322
+ function () {
323
+ var c = db[ns].find(condition);
324
+ var map = new Map();
325
+ var reduce_function = #{reduce};
326
+ while (c.hasNext()) {
327
+ var obj = c.next();
328
+
329
+ var key = {};
330
+ for (var i = 0; i < keys.length; i++) {
331
+ var k = keys[i];
332
+ key[k] = obj[k];
333
+ }
334
+
335
+ var aggObj = map.get(key);
336
+ if (aggObj == null) {
337
+ var newObj = Object.extend({}, key);
338
+ aggObj = Object.extend(newObj, initial);
339
+ map.put(key, aggObj);
340
+ }
341
+ reduce_function(obj, aggObj);
342
+ }
343
+ return {"result": map.values()};
344
+ }
345
+ EOS
346
+ return @db.eval(Code.new(group_function, scope))["result"]
347
+ end
348
+
349
+ # Rename this collection.
350
+ #
351
+ # If operating in auth mode, client must be authorized as an admin to
352
+ # perform this operation. Raises +InvalidName+ if +new_name+ is an invalid
353
+ # collection name.
354
+ #
355
+ # :new_name :: new name for this collection
356
+ def rename(new_name)
357
+ case new_name
358
+ when Symbol, String
359
+ else
360
+ raise TypeError, "new_name must be a string or symbol"
361
+ end
362
+
363
+ new_name = new_name.to_s
364
+
365
+ if new_name.empty? or new_name.include? ".."
366
+ raise InvalidName, "collection names cannot be empty"
367
+ end
368
+ if new_name.include? "$"
369
+ raise InvalidName, "collection names must not contain '$'"
370
+ end
371
+ if new_name.match(/^\./) or new_name.match(/\.$/)
372
+ raise InvalidName, "collection names must not start or end with '.'"
373
+ end
374
+
375
+ @db.rename_collection(@name, new_name)
376
+ end
377
+
378
+ # Get information on the indexes for the collection +collection_name+.
379
+ # Returns a hash where the keys are index names (as returned by
380
+ # Collection#create_index and the values are lists of [key, direction]
381
+ # pairs specifying the index (as passed to Collection#create_index).
382
+ def index_information
383
+ @db.index_information(@name)
384
+ end
385
+
386
+ # Return a hash containing options that apply to this collection.
387
+ # 'create' will be the collection name. For the other possible keys
388
+ # and values, see DB#create_collection.
389
+ def options
390
+ @db.collections_info(@name).next_object()['options']
391
+ end
392
+
393
+ # Get the number of documents in this collection.
394
+ def count()
395
+ find().count()
396
+ end
397
+
398
+ protected
399
+
400
+ def normalize_hint_fields(hint)
401
+ case hint
402
+ when String
403
+ {hint => 1}
404
+ when Hash
405
+ hint
406
+ when nil
407
+ nil
408
+ else
409
+ h = OrderedHash.new
410
+ hint.to_a.each { |k| h[k] = 1 }
411
+ h
149
412
  end
150
413
  end
151
414
  end
152
415
  end
153
-