mongo 0.1.0 → 0.15

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