mongo-find_replace 0.18.3

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 (68) hide show
  1. data/LICENSE.txt +202 -0
  2. data/README.rdoc +358 -0
  3. data/Rakefile +133 -0
  4. data/bin/bson_benchmark.rb +59 -0
  5. data/bin/fail_if_no_c.rb +11 -0
  6. data/examples/admin.rb +42 -0
  7. data/examples/capped.rb +22 -0
  8. data/examples/cursor.rb +48 -0
  9. data/examples/gridfs.rb +88 -0
  10. data/examples/index_test.rb +126 -0
  11. data/examples/info.rb +31 -0
  12. data/examples/queries.rb +70 -0
  13. data/examples/simple.rb +24 -0
  14. data/examples/strict.rb +35 -0
  15. data/examples/types.rb +36 -0
  16. data/lib/mongo.rb +61 -0
  17. data/lib/mongo/admin.rb +95 -0
  18. data/lib/mongo/collection.rb +664 -0
  19. data/lib/mongo/connection.rb +555 -0
  20. data/lib/mongo/cursor.rb +393 -0
  21. data/lib/mongo/db.rb +527 -0
  22. data/lib/mongo/exceptions.rb +60 -0
  23. data/lib/mongo/gridfs.rb +22 -0
  24. data/lib/mongo/gridfs/chunk.rb +90 -0
  25. data/lib/mongo/gridfs/grid_store.rb +555 -0
  26. data/lib/mongo/types/binary.rb +48 -0
  27. data/lib/mongo/types/code.rb +36 -0
  28. data/lib/mongo/types/dbref.rb +38 -0
  29. data/lib/mongo/types/min_max_keys.rb +58 -0
  30. data/lib/mongo/types/objectid.rb +219 -0
  31. data/lib/mongo/types/regexp_of_holding.rb +45 -0
  32. data/lib/mongo/util/bson_c.rb +18 -0
  33. data/lib/mongo/util/bson_ruby.rb +595 -0
  34. data/lib/mongo/util/byte_buffer.rb +222 -0
  35. data/lib/mongo/util/conversions.rb +97 -0
  36. data/lib/mongo/util/ordered_hash.rb +135 -0
  37. data/lib/mongo/util/server_version.rb +69 -0
  38. data/lib/mongo/util/support.rb +26 -0
  39. data/lib/mongo/util/xml_to_ruby.rb +112 -0
  40. data/mongo-ruby-driver.gemspec +28 -0
  41. data/test/replica/count_test.rb +34 -0
  42. data/test/replica/insert_test.rb +50 -0
  43. data/test/replica/pooled_insert_test.rb +54 -0
  44. data/test/replica/query_test.rb +39 -0
  45. data/test/test_admin.rb +67 -0
  46. data/test/test_bson.rb +397 -0
  47. data/test/test_byte_buffer.rb +81 -0
  48. data/test/test_chunk.rb +82 -0
  49. data/test/test_collection.rb +534 -0
  50. data/test/test_connection.rb +160 -0
  51. data/test/test_conversions.rb +120 -0
  52. data/test/test_cursor.rb +386 -0
  53. data/test/test_db.rb +254 -0
  54. data/test/test_db_api.rb +783 -0
  55. data/test/test_db_connection.rb +16 -0
  56. data/test/test_grid_store.rb +306 -0
  57. data/test/test_helper.rb +42 -0
  58. data/test/test_objectid.rb +156 -0
  59. data/test/test_ordered_hash.rb +168 -0
  60. data/test/test_round_trip.rb +114 -0
  61. data/test/test_slave_connection.rb +36 -0
  62. data/test/test_threading.rb +87 -0
  63. data/test/threading/test_threading_large_pool.rb +90 -0
  64. data/test/unit/collection_test.rb +52 -0
  65. data/test/unit/connection_test.rb +59 -0
  66. data/test/unit/cursor_test.rb +94 -0
  67. data/test/unit/db_test.rb +97 -0
  68. metadata +123 -0
data/examples/info.rb ADDED
@@ -0,0 +1,31 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+
3
+ require 'mongo'
4
+
5
+ include Mongo
6
+
7
+ host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost'
8
+ port = ENV['MONGO_RUBY_DRIVER_PORT'] || Connection::DEFAULT_PORT
9
+
10
+ puts "Connecting to #{host}:#{port}"
11
+ db = Connection.new(host, port).db('ruby-mongo-examples')
12
+ coll = db.collection('test')
13
+
14
+ # Erase all records from collection, if any
15
+ coll.remove
16
+
17
+ # Insert 3 records
18
+ 3.times { |i| coll.insert({'a' => i+1}) }
19
+
20
+ # Collection names in database
21
+ p db.collection_names
22
+
23
+ # More information about each collection
24
+ p db.collections_info
25
+
26
+ # Index information
27
+ db.create_index('test', 'a')
28
+ p db.index_information('test')
29
+
30
+ # Destroy the collection
31
+ coll.drop
@@ -0,0 +1,70 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+
3
+ require 'mongo'
4
+ require 'pp'
5
+
6
+ include Mongo
7
+
8
+ host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost'
9
+ port = ENV['MONGO_RUBY_DRIVER_PORT'] || Connection::DEFAULT_PORT
10
+
11
+ puts "Connecting to #{host}:#{port}"
12
+ db = Connection.new(host, port).db('ruby-mongo-examples')
13
+ coll = db.collection('test')
14
+
15
+ # Remove all records, if any
16
+ coll.remove
17
+
18
+ # Insert three records
19
+ coll.insert('a' => 1)
20
+ coll.insert('a' => 2)
21
+ coll.insert('b' => 3)
22
+
23
+ # Count.
24
+ puts "There are #{coll.count()} records."
25
+
26
+ # Find all records. find() returns a Cursor.
27
+ cursor = coll.find()
28
+
29
+ # Print them. Note that all records have an _id automatically added by the
30
+ # database. See pk.rb for an example of how to use a primary key factory to
31
+ # generate your own values for _id.
32
+ cursor.each { |row| pp row }
33
+
34
+ # Cursor has a to_a method that slurps all records into memory.
35
+ rows = coll.find().to_a
36
+ rows.each { |row| pp row }
37
+
38
+ # See Collection#find. From now on in this file, we won't be printing the
39
+ # records we find.
40
+ coll.find('a' => 1)
41
+
42
+ # Find records sort by 'a', skip 1, limit 2 records.
43
+ # Sort can be single name, array, or hash.
44
+ coll.find({}, {:skip => 1, :limit => 2, :sort => 'a'})
45
+
46
+ # Find all records with 'a' > 1. There is also $lt, $gte, and $lte.
47
+ coll.find({'a' => {'$gt' => 1}})
48
+ coll.find({'a' => {'$gt' => 1, '$lte' => 3}})
49
+
50
+ # Find all records with 'a' in a set of values.
51
+ coll.find('a' => {'$in' => [1,2]})
52
+
53
+ # Find by regexp
54
+ coll.find('a' => /[1|2]/)
55
+
56
+ # Print query explanation
57
+ pp coll.find('a' => /[1|2]/).explain()
58
+
59
+ # Use a hint with a query. Need an index. Hints can be stored with the
60
+ # collection, in which case they will be used with all queries, or they can be
61
+ # specified per query, in which case that hint overrides the hint associated
62
+ # with the collection if any.
63
+ coll.create_index('a')
64
+ coll.hint = 'a'
65
+
66
+ # You will see a different explanation now that the hint is in place
67
+ pp coll.find('a' => /[1|2]/).explain()
68
+
69
+ # Override hint for single query
70
+ coll.find({'a' => 1}, :hint => 'b')
@@ -0,0 +1,24 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+
3
+ require 'mongo'
4
+
5
+ include Mongo
6
+
7
+ host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost'
8
+ port = ENV['MONGO_RUBY_DRIVER_PORT'] || Connection::DEFAULT_PORT
9
+
10
+ puts "Connecting to #{host}:#{port}"
11
+ db = Connection.new(host, port).db('ruby-mongo-examples')
12
+ coll = db.collection('test')
13
+
14
+ # Erase all records from collection, if any
15
+ coll.remove
16
+
17
+ # Insert 3 records
18
+ 3.times { |i| coll.insert({'a' => i+1}) }
19
+
20
+ puts "There are #{coll.count()} records in the test collection. Here they are:"
21
+ coll.find().each { |doc| puts doc.inspect }
22
+
23
+ # Destroy the collection
24
+ coll.drop
@@ -0,0 +1,35 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+
3
+ require 'mongo'
4
+
5
+ include Mongo
6
+
7
+ host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost'
8
+ port = ENV['MONGO_RUBY_DRIVER_PORT'] || Connection::DEFAULT_PORT
9
+
10
+ puts "Connecting to #{host}:#{port}"
11
+ db = Connection.new(host, port).db('ruby-mongo-examples')
12
+
13
+ db.drop_collection('does-not-exist')
14
+ db.create_collection('test')
15
+
16
+ db.strict = true
17
+
18
+ begin
19
+ # Can't reference collection that does not exist
20
+ db.collection('does-not-exist')
21
+ puts "error: expected exception"
22
+ rescue => ex
23
+ puts "expected exception: #{ex}"
24
+ end
25
+
26
+ begin
27
+ # Can't create collection that already exists
28
+ db.create_collection('test')
29
+ puts "error: expected exception"
30
+ rescue => ex
31
+ puts "expected exception: #{ex}"
32
+ end
33
+
34
+ db.strict = false
35
+ db.drop_collection('test')
data/examples/types.rb ADDED
@@ -0,0 +1,36 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+
3
+ require 'mongo'
4
+ require 'pp'
5
+
6
+ include Mongo
7
+
8
+ host = ENV['MONGO_RUBY_DRIVER_HOST'] || 'localhost'
9
+ port = ENV['MONGO_RUBY_DRIVER_PORT'] || Connection::DEFAULT_PORT
10
+
11
+ puts "Connecting to #{host}:#{port}"
12
+ db = Connection.new(host, port).db('ruby-mongo-examples')
13
+ coll = db.collection('test')
14
+
15
+ # Remove all records, if any
16
+ coll.remove
17
+
18
+ # Insert record with all sorts of values
19
+ coll.insert('array' => [1, 2, 3],
20
+ 'string' => 'hello',
21
+ 'hash' => {'a' => 1, 'b' => 2},
22
+ 'date' => Time.now, # milliseconds only; microseconds are not stored
23
+ 'oid' => ObjectID.new,
24
+ 'binary' => Binary.new([1, 2, 3]),
25
+ 'int' => 42,
26
+ 'float' => 33.33333,
27
+ 'regex' => /foobar/i,
28
+ 'boolean' => true,
29
+ 'where' => Code.new('this.x == 3'),
30
+ 'dbref' => DBRef.new(coll.name, ObjectID.new),
31
+ 'null' => nil,
32
+ 'symbol' => :zildjian)
33
+
34
+ pp coll.find().next_document
35
+
36
+ coll.remove
data/lib/mongo.rb ADDED
@@ -0,0 +1,61 @@
1
+ $:.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+
3
+ module Mongo
4
+ VERSION = "0.18.3"
5
+ end
6
+
7
+ begin
8
+ # Need this for running test with and without c ext in Ruby 1.9.
9
+ raise LoadError if ENV['TEST_MODE'] && !ENV['C_EXT']
10
+ require 'mongo_ext/cbson'
11
+ raise LoadError unless defined?(CBson::VERSION) && CBson::VERSION == Mongo::VERSION
12
+ require 'mongo/util/bson_c'
13
+ BSON = BSON_C
14
+ rescue LoadError
15
+ require 'mongo/util/bson_ruby'
16
+ BSON = BSON_RUBY
17
+ warn "\n**Notice: C extension not loaded. This is required for optimum MongoDB Ruby driver performance."
18
+ warn " You can install the extension as follows:\n gem install mongo_ext\n"
19
+ warn " If you continue to receive this message after installing, make sure that the"
20
+ warn " mongo_ext gem is in your load path and that the mongo_ext and mongo gems are of the same version.\n"
21
+ end
22
+
23
+ module Mongo
24
+ ASCENDING = 1
25
+ DESCENDING = -1
26
+
27
+ module Constants
28
+ OP_REPLY = 1
29
+ OP_MSG = 1000
30
+ OP_UPDATE = 2001
31
+ OP_INSERT = 2002
32
+ OP_QUERY = 2004
33
+ OP_GET_MORE = 2005
34
+ OP_DELETE = 2006
35
+ OP_KILL_CURSORS = 2007
36
+
37
+ OP_QUERY_SLAVE_OK = 4
38
+ OP_QUERY_NO_CURSOR_TIMEOUT = 16
39
+ end
40
+
41
+ end
42
+
43
+ require 'mongo/types/binary'
44
+ require 'mongo/types/code'
45
+ require 'mongo/types/dbref'
46
+ require 'mongo/types/objectid'
47
+ require 'mongo/types/regexp_of_holding'
48
+ require 'mongo/types/min_max_keys'
49
+
50
+ require 'mongo/util/support'
51
+ require 'mongo/util/conversions'
52
+ require 'mongo/util/server_version'
53
+ require 'mongo/util/bson_ruby'
54
+
55
+ require 'mongo/admin'
56
+ require 'mongo/collection'
57
+ require 'mongo/connection'
58
+ require 'mongo/cursor'
59
+ require 'mongo/db'
60
+ require 'mongo/exceptions'
61
+ require 'mongo/gridfs'
@@ -0,0 +1,95 @@
1
+ # --
2
+ # Copyright (C) 2008-2009 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
+ module Mongo
18
+
19
+ # @deprecated this class is deprecated. Methods defined here will
20
+ # henceforth be available in Mongo::DB.
21
+ class Admin
22
+
23
+ def initialize(db)
24
+ warn "The Admin class has been DEPRECATED. All admin methods now exist in DB."
25
+ @db = db
26
+ end
27
+
28
+ # Return the current database profiling level.
29
+ #
30
+ # @return [Symbol] :off, :slow_only, or :all
31
+ #
32
+ # @deprecated please use DB#profiling_level instead.
33
+ def profiling_level
34
+ warn "Admin#profiling_level has been DEPRECATED. Please use DB#profiling_level instead."
35
+ oh = OrderedHash.new
36
+ oh[:profile] = -1
37
+ doc = @db.command(oh)
38
+ raise "Error with profile command: #{doc.inspect}" unless @db.ok?(doc) && doc['was'].kind_of?(Numeric)
39
+ case doc['was'].to_i
40
+ when 0
41
+ :off
42
+ when 1
43
+ :slow_only
44
+ when 2
45
+ :all
46
+ else
47
+ raise "Error: illegal profiling level value #{doc['was']}"
48
+ end
49
+ end
50
+
51
+ # Set database profiling level to :off, :slow_only, or :all.
52
+ #
53
+ # @deprecated please use DB#profiling_level= instead.
54
+ def profiling_level=(level)
55
+ warn "Admin#profiling_level= has been DEPRECATED. Please use DB#profiling_level= instead."
56
+ oh = OrderedHash.new
57
+ oh[:profile] = case level
58
+ when :off
59
+ 0
60
+ when :slow_only
61
+ 1
62
+ when :all
63
+ 2
64
+ else
65
+ raise "Error: illegal profiling level value #{level}"
66
+ end
67
+ doc = @db.command(oh)
68
+ raise "Error with profile command: #{doc.inspect}" unless @db.ok?(doc)
69
+ end
70
+
71
+ # Returns an array containing current profiling information.
72
+ #
73
+ # @deprecated please use DB#profiling_info instead.
74
+ def profiling_info
75
+ warn "Admin#profiling_info has been DEPRECATED. Please use DB#profiling_info instead."
76
+ Cursor.new(Collection.new(@db, DB::SYSTEM_PROFILE_COLLECTION), :selector => {}).to_a
77
+ end
78
+
79
+ # Validate a named collection by raising an exception if there is a
80
+ # problem or returning an interesting hash (see especially the
81
+ # 'result' string value) if all is well.
82
+ #
83
+ # @deprecated please use DB#validate_collection instead.
84
+ def validate_collection(name)
85
+ warn "Admin#validate_collection has been DEPRECATED. Please use DB#validate_collection instead."
86
+ doc = @db.command(:validate => name)
87
+ raise "Error with validate command: #{doc.inspect}" unless @db.ok?(doc)
88
+ result = doc['result']
89
+ raise "Error with validation data: #{doc.inspect}" unless result.kind_of?(String)
90
+ raise "Error: invalid collection #{name}: #{doc.inspect}" if result =~ /\b(exception|corrupt)\b/i
91
+ doc
92
+ end
93
+
94
+ end
95
+ end
@@ -0,0 +1,664 @@
1
+ # --
2
+ # Copyright (C) 2008-2009 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
+ module Mongo
18
+
19
+ # A named collection of documents in a database.
20
+ class Collection
21
+ include Mongo::Conversions
22
+
23
+ attr_reader :db, :name, :pk_factory, :hint
24
+
25
+ # Initialize a collection object.
26
+ #
27
+ # @param [DB] db a MongoDB database instance.
28
+ # @param [String, Symbol] name the name of the collection.
29
+ #
30
+ # @raise [InvalidName]
31
+ # if collection name is empty, contains '$', or starts or ends with '.'
32
+ #
33
+ # @raise [TypeError]
34
+ # if collection name is not a string or symbol
35
+ #
36
+ # @return [Collection]
37
+ def initialize(db, name, pk_factory=nil)
38
+ case name
39
+ when Symbol, String
40
+ else
41
+ raise TypeError, "new_name must be a string or symbol"
42
+ end
43
+
44
+ name = name.to_s
45
+
46
+ if name.empty? or name.include? ".."
47
+ raise InvalidName, "collection names cannot be empty"
48
+ end
49
+ if name.include? "$"
50
+ raise InvalidName, "collection names must not contain '$'" unless name =~ /((^\$cmd)|(oplog\.\$main))/
51
+ end
52
+ if name.match(/^\./) or name.match(/\.$/)
53
+ raise InvalidName, "collection names must not start or end with '.'"
54
+ end
55
+
56
+ @db, @name = db, name
57
+ @connection = @db.connection
58
+ @pk_factory = pk_factory || ObjectID
59
+ @hint = nil
60
+ end
61
+
62
+ # Return a sub-collection of this collection by name. If 'users' is a collection, then
63
+ # 'users.comments' is a sub-collection of users.
64
+ #
65
+ # @param [String] name
66
+ # the collection to return
67
+ #
68
+ # @raise [InvalidName]
69
+ # if passed an invalid collection name
70
+ #
71
+ # @return [Collection]
72
+ # the specified sub-collection
73
+ def [](name)
74
+ name = "#{self.name}.#{name}"
75
+ return Collection.new(db, name) if !db.strict? || db.collection_names.include?(name)
76
+ raise "Collection #{name} doesn't exist. Currently in strict mode."
77
+ end
78
+
79
+ # Set a hint field for query optimizer. Hint may be a single field
80
+ # name, array of field names, or a hash (preferably an [OrderedHash]).
81
+ # If using MongoDB > 1.1, you probably don't ever need to set a hint.
82
+ #
83
+ # @param [String, Array, OrderedHash] hint a single field, an array of
84
+ # fields, or a hash specifying fields
85
+ def hint=(hint=nil)
86
+ @hint = normalize_hint_fields(hint)
87
+ self
88
+ end
89
+
90
+ # Query the database.
91
+ #
92
+ # The +selector+ argument is a prototype document that all results must
93
+ # match. For example:
94
+ #
95
+ # collection.find({"hello" => "world"})
96
+ #
97
+ # only matches documents that have a key "hello" with value "world".
98
+ # Matches can have other keys *in addition* to "hello".
99
+ #
100
+ # If given an optional block +find+ will yield a Cursor to that block,
101
+ # close the cursor, and then return nil. This guarantees that partially
102
+ # evaluated cursors will be closed. If given no block +find+ returns a
103
+ # cursor.
104
+ #
105
+ # @param [Hash] selector
106
+ # a document specifying elements which must be present for a
107
+ # document to be included in the result set.
108
+ #
109
+ # @option opts [Array] :fields field names that should be returned in the result
110
+ # set ("_id" will always be included). By limiting results to a certain subset of fields,
111
+ # you can cut down on network traffic and decoding time.
112
+ # @option opts [Integer] :skip number of documents to skip from the beginning of the result set
113
+ # @option opts [Integer] :limit maximum number of documents to return
114
+ # @option opts [Array] :sort an array of [key, direction] pairs to sort by. Direction should
115
+ # be specified as Mongo::ASCENDING (or :ascending / :asc) or Mongo::DESCENDING (or :descending / :desc)
116
+ # @option opts [String, Array, OrderedHash] :hint hint for query optimizer, usually not necessary if using MongoDB > 1.1
117
+ # @option opts [Boolean] :snapshot ('false') if true, snapshot mode will be used for this query.
118
+ # Snapshot mode assures no duplicates are returned, or objects missed, which were preset at both the start and
119
+ # end of the query's execution. For details see http://www.mongodb.org/display/DOCS/How+to+do+Snapshotting+in+the+Mongo+Database
120
+ # @option opts [Boolean] :timeout ('true') when +true+, the returned cursor will be subject to
121
+ # the normal cursor timeout behavior of the mongod process. When +false+, the returned cursor will never timeout. Note
122
+ # that disabling timeout will only work when #find is invoked with a block. This is to prevent any inadvertant failure to
123
+ # close the cursor, as the cursor is explicitly closed when block code finishes.
124
+ #
125
+ # @raise [ArgumentError]
126
+ # if timeout is set to false and find is not invoked in a block
127
+ #
128
+ # @raise [RuntimeError]
129
+ # if given unknown options
130
+ def find(selector={}, opts={})
131
+ fields = opts.delete(:fields)
132
+ fields = ["_id"] if fields && fields.empty?
133
+ skip = opts.delete(:skip) || skip || 0
134
+ limit = opts.delete(:limit) || 0
135
+ sort = opts.delete(:sort)
136
+ hint = opts.delete(:hint)
137
+ snapshot = opts.delete(:snapshot)
138
+ if opts[:timeout] == false && !block_given?
139
+ raise ArgumentError, "Timeout can be set to false only when #find is invoked with a block."
140
+ end
141
+ timeout = block_given? ? (opts.delete(:timeout) || true) : true
142
+ if hint
143
+ hint = normalize_hint_fields(hint)
144
+ else
145
+ hint = @hint # assumed to be normalized already
146
+ end
147
+ raise RuntimeError, "Unknown options [#{opts.inspect}]" unless opts.empty?
148
+
149
+ cursor = Cursor.new(self, :selector => selector, :fields => fields, :skip => skip, :limit => limit,
150
+ :order => sort, :hint => hint, :snapshot => snapshot, :timeout => timeout)
151
+ if block_given?
152
+ yield cursor
153
+ cursor.close()
154
+ nil
155
+ else
156
+ cursor
157
+ end
158
+ end
159
+
160
+ # Find and Modify (or Remove)
161
+ # This command is useful to atomically change an object and then get back the results
162
+ #
163
+ # @option opts [Hash] :query ({}) a query selector document, like what's passed to #find, to limit
164
+ # the operation to a subset of the collection.
165
+ # @option opts [Array] :sort ([]) an array of [key, direction] pairs to sort by. Direction should
166
+ # be specified as Mongo::ASCENDING (or :ascending / :asc) or Mongo::DESCENDING (or :descending / :desc)
167
+ # @option opts [Boolean] :remove (false) set to a true to remove the object before returning
168
+ # @option opts [Hash] :update ({}) a modifier object that updates the document
169
+ # @option opts [Boolean] :new (false) if true, return the modified object rather than the original. Ignored for remove.
170
+ #
171
+ # @return [Hash] The document from the database
172
+ #
173
+ # @see http://www.mongodb.org/display/DOCS/findandmodify+Command
174
+ def find_modify(opts={})
175
+ hash = OrderedHash.new
176
+ hash['findandmodify'] = self.name
177
+
178
+ sort = opts.delete(:sort)
179
+ hash[:sort] = formatted_sort_clause(sort) if sort
180
+
181
+ hash.merge! opts
182
+
183
+ result = @db.command(hash)
184
+ unless result["ok"] == 1
185
+ raise Mongo::OperationFailure, "find_modify failed: #{result['errmsg']}"
186
+ end
187
+
188
+ document = result['value']
189
+ end
190
+
191
+ # Return a single object from the database.
192
+ #
193
+ # @return [OrderedHash, Nil]
194
+ # a single document or nil if no result is found.
195
+ #
196
+ # @param [Hash, ObjectID, Nil] spec_or_object_id a hash specifying elements
197
+ # which must be present for a document to be included in the result set or an
198
+ # instance of ObjectID to be used as the value for an _id query.
199
+ # If nil, an empty selector, {}, will be used.
200
+ #
201
+ # @option opts [Hash]
202
+ # any valid options that can be send to Collection#find
203
+ #
204
+ # @raise [TypeError]
205
+ # if the argument is of an improper type.
206
+ def find_one(spec_or_object_id=nil, opts={})
207
+ spec = case spec_or_object_id
208
+ when nil
209
+ {}
210
+ when ObjectID
211
+ {:_id => spec_or_object_id}
212
+ when Hash
213
+ spec_or_object_id
214
+ else
215
+ raise TypeError, "spec_or_object_id must be an instance of ObjectID or Hash, or nil"
216
+ end
217
+ find(spec, opts.merge(:limit => -1)).next_document
218
+ end
219
+
220
+ # Save a document to this collection.
221
+ #
222
+ # @param [Hash] doc
223
+ # the document to be saved. If the document already has an '_id' key,
224
+ # then an update (upsert) operation will be performed, and any existing
225
+ # document with that _id is overwritten. Otherwise an insert operation is performed.
226
+ #
227
+ # @return [ObjectID] the _id of the saved document.
228
+ #
229
+ # @option opts [Boolean] :safe (+false+)
230
+ # If true, check that the save succeeded. OperationFailure
231
+ # will be raised on an error. Note that a safe check requires an extra
232
+ # round-trip to the database.
233
+ def save(doc, options={})
234
+ if doc.has_key?(:_id) || doc.has_key?('_id')
235
+ id = doc[:_id] || doc['_id']
236
+ update({:_id => id}, doc, :upsert => true, :safe => options.delete(:safe))
237
+ id
238
+ else
239
+ insert(doc, :safe => options.delete(:safe))
240
+ end
241
+ end
242
+
243
+ # Insert one or more documents into the collection.
244
+ #
245
+ # @param [Hash, Array] doc_or_docs
246
+ # a document (as a hash) or array of documents to be inserted.
247
+ #
248
+ # @return [ObjectID, Array]
249
+ # the _id of the inserted document or a list of _ids of all inserted documents.
250
+ # Note: the object may have been modified by the database's PK factory, if it has one.
251
+ #
252
+ # @option opts [Boolean] :safe (+false+)
253
+ # If true, check that the save succeeded. OperationFailure
254
+ # will be raised on an error. Note that a safe check requires an extra
255
+ # round-trip to the database.
256
+ def insert(doc_or_docs, options={})
257
+ doc_or_docs = [doc_or_docs] unless doc_or_docs.is_a?(Array)
258
+ doc_or_docs.collect! { |doc| @pk_factory.create_pk(doc) }
259
+ result = insert_documents(doc_or_docs, @name, true, options[:safe])
260
+ result.size > 1 ? result : result.first
261
+ end
262
+ alias_method :<<, :insert
263
+
264
+ # Remove all documents from this collection.
265
+ #
266
+ # @param [Hash] selector
267
+ # If specified, only matching documents will be removed.
268
+ #
269
+ # @option opts [Boolean] :safe [false] run the operation in safe mode, which
270
+ # will call :getlasterror on the database and report any assertions.
271
+ #
272
+ # @example remove all documents from the 'users' collection:
273
+ # users.remove
274
+ # users.remove({})
275
+ #
276
+ # @example remove only documents that have expired:
277
+ # users.remove({:expire => {"$lte" => Time.now}})
278
+ #
279
+ # @return [True]
280
+ #
281
+ # @raise [Mongo::OperationFailure] an exception will be raised iff safe mode is enabled
282
+ # and the operation fails.
283
+ def remove(selector={}, opts={})
284
+ # Initial byte is 0.
285
+ message = ByteBuffer.new([0, 0, 0, 0])
286
+ BSON_RUBY.serialize_cstr(message, "#{@db.name}.#{@name}")
287
+ message.put_int(0)
288
+ message.put_array(BSON.serialize(selector, false).to_a)
289
+
290
+ if opts[:safe]
291
+ @connection.send_message_with_safe_check(Mongo::Constants::OP_DELETE, message,
292
+ "db.#{@db.name}.remove(#{selector.inspect})")
293
+ # the return value of send_message_with_safe_check isn't actually meaningful --
294
+ # only the fact that it didn't raise an error is -- so just return true
295
+ true
296
+ else
297
+ @connection.send_message(Mongo::Constants::OP_DELETE, message,
298
+ "db.#{@db.name}.remove(#{selector.inspect})")
299
+ end
300
+ end
301
+
302
+ # Update a single document in this collection.
303
+ #
304
+ # @param [Hash] selector
305
+ # a hash specifying elements which must be present for a document to be updated. Note:
306
+ # the update command currently updates only the first document matching the
307
+ # given selector. If you want all matching documents to be updated, be sure
308
+ # to specify :multi => true.
309
+ # @param [Hash] document
310
+ # a hash specifying the fields to be changed in the selected document,
311
+ # or (in the case of an upsert) the document to be inserted
312
+ #
313
+ # @option [Boolean] :upsert (+false+) if true, performs an upsert (update or insert)
314
+ # @option [Boolean] :multi (+false+) update all documents matching the selector, as opposed to
315
+ # just the first matching document. Note: only works in MongoDB 1.1.3 or later.
316
+ # @option opts [Boolean] :safe (+false+)
317
+ # If true, check that the save succeeded. OperationFailure
318
+ # will be raised on an error. Note that a safe check requires an extra
319
+ # round-trip to the database.
320
+ def update(selector, document, options={})
321
+ # Initial byte is 0.
322
+ message = ByteBuffer.new([0, 0, 0, 0])
323
+ BSON_RUBY.serialize_cstr(message, "#{@db.name}.#{@name}")
324
+ update_options = 0
325
+ update_options += 1 if options[:upsert]
326
+ update_options += 2 if options[:multi]
327
+ message.put_int(update_options)
328
+ message.put_array(BSON.serialize(selector, false).to_a)
329
+ message.put_array(BSON.serialize(document, false).to_a)
330
+ if options[:safe]
331
+ @connection.send_message_with_safe_check(Mongo::Constants::OP_UPDATE, message, @db.name,
332
+ "db.#{@name}.update(#{selector.inspect}, #{document.inspect})")
333
+ else
334
+ @connection.send_message(Mongo::Constants::OP_UPDATE, message,
335
+ "db.#{@name}.update(#{selector.inspect}, #{document.inspect})")
336
+ end
337
+ end
338
+
339
+ # Create a new index.
340
+ #
341
+ # @param [String, Array] field_or_spec
342
+ # should be either a single field name or an array of
343
+ # [field name, direction] pairs. Directions should be specified as Mongo::ASCENDING or Mongo::DESCENDING.
344
+ #
345
+ # @param [Boolean] unique if true, this index will enforce a uniqueness constraint.
346
+ #
347
+ # @return [String] the name of the index created.
348
+ def create_index(field_or_spec, unique=false)
349
+ field_h = OrderedHash.new
350
+ if field_or_spec.is_a?(String) || field_or_spec.is_a?(Symbol)
351
+ field_h[field_or_spec.to_s] = 1
352
+ else
353
+ field_or_spec.each { |f| field_h[f[0].to_s] = f[1] }
354
+ end
355
+ name = generate_index_names(field_h)
356
+ sel = {
357
+ :name => name,
358
+ :ns => "#{@db.name}.#{@name}",
359
+ :key => field_h,
360
+ :unique => unique }
361
+ insert_documents([sel], Mongo::DB::SYSTEM_INDEX_COLLECTION, false)
362
+ name
363
+ end
364
+
365
+ # Drop a specified index.
366
+ #
367
+ # @param [String] name
368
+ def drop_index(name)
369
+ @db.drop_index(@name, name)
370
+ end
371
+
372
+ # Drop all indexes.
373
+ def drop_indexes
374
+
375
+ # Note: calling drop_indexes with no args will drop them all.
376
+ @db.drop_index(@name, '*')
377
+
378
+ end
379
+
380
+ # Drop the entire collection. USE WITH CAUTION.
381
+ def drop
382
+ @db.drop_collection(@name)
383
+ end
384
+
385
+ # Perform a map/reduce operation on the current collection.
386
+ #
387
+ # @param [String, Code] map a map function, written in JavaScript.
388
+ # @param [String, Code] reduce a reduce function, written in JavaScript.
389
+ #
390
+ # @option opts [Hash] :query ({}) a query selector document, like what's passed to #find, to limit
391
+ # the operation to a subset of the collection.
392
+ # @option opts [Array] :sort ([]) an array of [key, direction] pairs to sort by. Direction should
393
+ # be specified as Mongo::ASCENDING (or :ascending / :asc) or Mongo::DESCENDING (or :descending / :desc)
394
+ # @option opts [Integer] :limit (nil) if passing a query, number of objects to return from the collection.
395
+ # @option opts [String, Code] :finalize (nil) a javascript function to apply to the result set after the
396
+ # map/reduce operation has finished.
397
+ # @option opts [String] :out (nil) the name of the output collection. If specified, the collection will not be treated as temporary.
398
+ # @option opts [Boolean] :keeptemp (false) if true, the generated collection will be persisted. default is false.
399
+ # @option opts [Boolean ] :verbose (false) if true, provides statistics on job execution time.
400
+ #
401
+ # @return [Collection] a collection containing the results of the operation.
402
+ #
403
+ # @see http://www.mongodb.org/display/DOCS/MapReduce Offical MongoDB map/reduce documentation.
404
+ def map_reduce(map, reduce, opts={})
405
+ map = Code.new(map) unless map.is_a?(Code)
406
+ reduce = Code.new(reduce) unless reduce.is_a?(Code)
407
+
408
+ hash = OrderedHash.new
409
+ hash['mapreduce'] = self.name
410
+ hash['map'] = map
411
+ hash['reduce'] = reduce
412
+ hash.merge! opts
413
+
414
+ result = @db.command(hash)
415
+ unless result["ok"] == 1
416
+ raise Mongo::OperationFailure, "map-reduce failed: #{result['errmsg']}"
417
+ end
418
+ @db[result["result"]]
419
+ end
420
+ alias :mapreduce :map_reduce
421
+
422
+ # Perform a group aggregation.
423
+ #
424
+ # @param [Array, String, Code, Nil] :key either 1) an array of fields to group by,
425
+ # 2) a javascript function to generate the key object, or 3) nil.
426
+ # @param [Hash] condition an optional document specifying a query to limit the documents over which group is run.
427
+ # @param [Hash] initial initial value of the aggregation counter object
428
+ # @param [String, Code] reduce aggregation function, in JavaScript
429
+ # @param [String, Code] finalize :: optional. a JavaScript function that receives and modifies
430
+ # each of the resultant grouped objects. Available only when group is run
431
+ # with command set to true.
432
+ # @param [Boolean] command if true, run the group as a command instead of in an
433
+ # eval. Note: Running group as eval has been DEPRECATED.
434
+ #
435
+ # @return [Array] the grouped items.
436
+ def group(key, condition, initial, reduce, command=false, finalize=nil)
437
+
438
+ if command
439
+
440
+ reduce = Code.new(reduce) unless reduce.is_a?(Code)
441
+
442
+ group_command = {
443
+ "group" => {
444
+ "ns" => @name,
445
+ "$reduce" => reduce,
446
+ "cond" => condition,
447
+ "initial" => initial
448
+ }
449
+ }
450
+
451
+ unless key.nil?
452
+ if key.is_a? Array
453
+ key_type = "key"
454
+ key_value = {}
455
+ key.each { |k| key_value[k] = 1 }
456
+ else
457
+ key_type = "$keyf"
458
+ key_value = key.is_a?(Code) ? key : Code.new(key)
459
+ end
460
+
461
+ group_command["group"][key_type] = key_value
462
+ end
463
+
464
+ # only add finalize if specified
465
+ if finalize
466
+ finalize = Code.new(finalize) unless finalize.is_a?(Code)
467
+ group_command['group']['finalize'] = finalize
468
+ end
469
+
470
+ result = @db.command group_command
471
+
472
+ if result["ok"] == 1
473
+ return result["retval"]
474
+ else
475
+ raise OperationFailure, "group command failed: #{result['errmsg']}"
476
+ end
477
+
478
+ else
479
+
480
+ warn "Collection#group must now be run as a command; you can do this by passing 'true' as the command argument."
481
+
482
+ raise OperationFailure, ":finalize can be specified only when " +
483
+ "group is run as a command (set command param to true)" if finalize
484
+
485
+ raise OperationFailure, "key must be an array of fields to group by. If you want to pass a key function,
486
+ run group as a command by passing 'true' as the command argument." unless key.is_a? Array || key.nil?
487
+
488
+ case reduce
489
+ when Code
490
+ scope = reduce.scope
491
+ else
492
+ scope = {}
493
+ end
494
+ scope.merge!({
495
+ "ns" => @name,
496
+ "keys" => key,
497
+ "condition" => condition,
498
+ "initial" => initial })
499
+
500
+ group_function = <<EOS
501
+ function () {
502
+ var c = db[ns].find(condition);
503
+ var map = new Map();
504
+ var reduce_function = #{reduce};
505
+ while (c.hasNext()) {
506
+ var obj = c.next();
507
+
508
+ var key = {};
509
+ for (var i = 0; i < keys.length; i++) {
510
+ var k = keys[i];
511
+ key[k] = obj[k];
512
+ }
513
+
514
+ var aggObj = map.get(key);
515
+ if (aggObj == null) {
516
+ var newObj = Object.extend({}, key);
517
+ aggObj = Object.extend(newObj, initial);
518
+ map.put(key, aggObj);
519
+ }
520
+ reduce_function(obj, aggObj);
521
+ }
522
+ return {"result": map.values()};
523
+ }
524
+ EOS
525
+ @db.eval(Code.new(group_function, scope))["result"]
526
+ end
527
+ end
528
+
529
+ # Return a list of distinct values for +key+ across all
530
+ # documents in the collection. The key may use dot notation
531
+ # to reach into an embedded object.
532
+ #
533
+ # @param [String, Symbol, OrderedHash] key or hash to group by.
534
+ # @param [Hash] query a selector for limiting the result set over which to group.
535
+ #
536
+ # @example Saving zip codes and ages and returning distinct results.
537
+ # @collection.save({:zip => 10010, :name => {:age => 27}})
538
+ # @collection.save({:zip => 94108, :name => {:age => 24}})
539
+ # @collection.save({:zip => 10010, :name => {:age => 27}})
540
+ # @collection.save({:zip => 99701, :name => {:age => 24}})
541
+ # @collection.save({:zip => 94108, :name => {:age => 27}})
542
+ #
543
+ # @collection.distinct(:zip)
544
+ # [10010, 94108, 99701]
545
+ # @collection.distinct("name.age")
546
+ # [27, 24]
547
+ #
548
+ # # You may also pass a document selector as the second parameter
549
+ # # to limit the documents over which distinct is run:
550
+ # @collection.distinct("name.age", {"name.age" => {"$gt" => 24}})
551
+ # [27]
552
+ #
553
+ # @return [Array] an array of distinct values.
554
+ def distinct(key, query=nil)
555
+ raise MongoArgumentError unless [String, Symbol].include?(key.class)
556
+ command = OrderedHash.new
557
+ command[:distinct] = @name
558
+ command[:key] = key.to_s
559
+ command[:query] = query
560
+
561
+ @db.command(command)["values"]
562
+ end
563
+
564
+ # Rename this collection.
565
+ #
566
+ # Note: If operating in auth mode, the client must be authorized as an admin to
567
+ # perform this operation.
568
+ #
569
+ # @param [String ] new_name the new name for this collection
570
+ #
571
+ # @raise [InvalidName] if +new_name+ is an invalid collection name.
572
+ def rename(new_name)
573
+ case new_name
574
+ when Symbol, String
575
+ else
576
+ raise TypeError, "new_name must be a string or symbol"
577
+ end
578
+
579
+ new_name = new_name.to_s
580
+
581
+ if new_name.empty? or new_name.include? ".."
582
+ raise InvalidName, "collection names cannot be empty"
583
+ end
584
+ if new_name.include? "$"
585
+ raise InvalidName, "collection names must not contain '$'"
586
+ end
587
+ if new_name.match(/^\./) or new_name.match(/\.$/)
588
+ raise InvalidName, "collection names must not start or end with '.'"
589
+ end
590
+
591
+ @db.rename_collection(@name, new_name)
592
+ end
593
+
594
+ # Get information on the indexes for this collection.
595
+ #
596
+ # @return [Hash] a hash where the keys are index names.
597
+ def index_information
598
+ @db.index_information(@name)
599
+ end
600
+
601
+ # Return a hash containing options that apply to this collection.
602
+ # For all possible keys and values, see DB#create_collection.
603
+ #
604
+ # @return [Hash] options that apply to this collection.
605
+ def options
606
+ @db.collections_info(@name).next_document['options']
607
+ end
608
+
609
+ # Get the number of documents in this collection.
610
+ #
611
+ # @return [Integer]
612
+ def count
613
+ find().count()
614
+ end
615
+
616
+ alias :size :count
617
+
618
+ protected
619
+
620
+ def normalize_hint_fields(hint)
621
+ case hint
622
+ when String
623
+ {hint => 1}
624
+ when Hash
625
+ hint
626
+ when nil
627
+ nil
628
+ else
629
+ h = OrderedHash.new
630
+ hint.to_a.each { |k| h[k] = 1 }
631
+ h
632
+ end
633
+ end
634
+
635
+ private
636
+
637
+ # Sends a Mongo::Constants::OP_INSERT message to the database.
638
+ # Takes an array of +documents+, an optional +collection_name+, and a
639
+ # +check_keys+ setting.
640
+ def insert_documents(documents, collection_name=@name, check_keys=true, safe=false)
641
+ # Initial byte is 0.
642
+ message = ByteBuffer.new([0, 0, 0, 0])
643
+ BSON_RUBY.serialize_cstr(message, "#{@db.name}.#{collection_name}")
644
+ documents.each { |doc| message.put_array(BSON.serialize(doc, check_keys).to_a) }
645
+ if safe
646
+ @connection.send_message_with_safe_check(Mongo::Constants::OP_INSERT, message, @db.name,
647
+ "db.#{collection_name}.insert(#{documents.inspect})")
648
+ else
649
+ @connection.send_message(Mongo::Constants::OP_INSERT, message,
650
+ "db.#{collection_name}.insert(#{documents.inspect})")
651
+ end
652
+ documents.collect { |o| o[:_id] || o['_id'] }
653
+ end
654
+
655
+ def generate_index_names(spec)
656
+ indexes = []
657
+ spec.each_pair do |field, direction|
658
+ indexes.push("#{field}_#{direction}")
659
+ end
660
+ indexes.join("_")
661
+ end
662
+ end
663
+
664
+ end