mongo-find_replace 0.18.3

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