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,20 +1,21 @@
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 'socket'
18
+ require 'digest/md5'
18
19
  require 'mutex_m'
19
20
  require 'mongo/collection'
20
21
  require 'mongo/message'
@@ -22,349 +23,533 @@ require 'mongo/query'
22
23
  require 'mongo/util/ordered_hash.rb'
23
24
  require 'mongo/admin'
24
25
 
25
- module XGen
26
- module Mongo
27
- module Driver
28
-
29
- # A Mongo database.
30
- class DB
31
-
32
- SYSTEM_NAMESPACE_COLLECTION = "system.namespaces"
33
- SYSTEM_INDEX_COLLECTION = "system.indexes"
34
- SYSTEM_PROFILE_COLLECTION = "system.profile"
35
- SYSTEM_COMMAND_COLLECTION = "$cmd"
36
-
37
- # Strict mode enforces collection existence checks. When +true+,
38
- # asking for a collection that does not exist or trying to create a
39
- # collection that already exists raises an error.
40
- #
41
- # Strict mode is off (+false+) by default. Its value can be changed at
42
- # any time.
43
- attr_writer :strict
44
-
45
- # Returns the value of the +strict+ flag.
46
- def strict?; @strict; end
47
-
48
- # The name of the database.
49
- attr_reader :name
50
-
51
- # Host to which we are currently connected.
52
- attr_reader :host
53
- # Port to which we are currently connected.
54
- attr_reader :port
55
-
56
- # An array of [host, port] pairs.
57
- attr_reader :nodes
58
-
59
- # The database's socket. For internal (and Cursor) use only.
60
- attr_reader :socket
61
-
62
- # db_name :: The database name
63
- #
64
- # nodes :: An array of [host, port] pairs.
65
- #
66
- # When a DB object first connects, it tries the first node. If that
67
- # fails, it keeps trying to connect to the remaining nodes until it
68
- # sucessfully connects.
69
- def initialize(db_name, nodes)
70
- raise "Invalid DB name" if !db_name || (db_name && db_name.length > 0 && db_name.include?("."))
71
- @name, @nodes = db_name, nodes
72
- @strict = false
73
- @semaphore = Object.new
74
- @semaphore.extend Mutex_m
75
- connect_to_first_available_host
26
+ module Mongo
27
+
28
+ # A Mongo database.
29
+ class DB
30
+
31
+ SYSTEM_NAMESPACE_COLLECTION = "system.namespaces"
32
+ SYSTEM_INDEX_COLLECTION = "system.indexes"
33
+ SYSTEM_PROFILE_COLLECTION = "system.profile"
34
+ SYSTEM_USER_COLLECTION = "system.users"
35
+ SYSTEM_COMMAND_COLLECTION = "$cmd"
36
+
37
+ # Strict mode enforces collection existence checks. When +true+,
38
+ # asking for a collection that does not exist or trying to create a
39
+ # collection that already exists raises an error.
40
+ #
41
+ # Strict mode is off (+false+) by default. Its value can be changed at
42
+ # any time.
43
+ attr_writer :strict
44
+
45
+ # Returns the value of the +strict+ flag.
46
+ def strict?; @strict; end
47
+
48
+ # The name of the database.
49
+ attr_reader :name
50
+
51
+ # Host to which we are currently connected.
52
+ attr_reader :host
53
+ # Port to which we are currently connected.
54
+ attr_reader :port
55
+
56
+ # An array of [host, port] pairs.
57
+ attr_reader :nodes
58
+
59
+ # The database's socket. For internal (and Cursor) use only.
60
+ attr_reader :socket
61
+
62
+ def slave_ok?; @slave_ok; end
63
+ def auto_reconnect?; @auto_reconnect; end
64
+
65
+ # A primary key factory object (or +nil+). See the README.doc file or
66
+ # DB#new for details.
67
+ attr_reader :pk_factory
68
+
69
+ def pk_factory=(pk_factory)
70
+ raise "error: can not change PK factory" if @pk_factory
71
+ @pk_factory = pk_factory
72
+ end
73
+
74
+ # Instances of DB are normally obtained by calling Mongo#db.
75
+ #
76
+ # db_name :: The database name
77
+ #
78
+ # nodes :: An array of [host, port] pairs. See Connection#new, which offers
79
+ # a more flexible way of defining nodes.
80
+ #
81
+ # options :: A hash of options.
82
+ #
83
+ # Options:
84
+ #
85
+ # :strict :: If true, collections must exist to be accessed and must
86
+ # not exist to be created. See #collection and
87
+ # #create_collection.
88
+ #
89
+ # :pk :: A primary key factory object that must respond to :create_pk,
90
+ # which should take a hash and return a hash which merges the
91
+ # original hash with any primary key fields the factory wishes
92
+ # to inject. (NOTE: if the object already has a primary key,
93
+ # the factory should not inject a new key; this means that the
94
+ # object is being used in a repsert but it already exists.) The
95
+ # idea here is that when ever a record is inserted, the :pk
96
+ # object's +create_pk+ method will be called and the new hash
97
+ # returned will be inserted.
98
+ #
99
+ # :slave_ok :: Only used if +nodes+ contains only one host/port. If
100
+ # false, when connecting to that host/port we check to
101
+ # see if the server is the master. If it is not, an error
102
+ # is thrown.
103
+ #
104
+ # :auto_reconnect :: If the connection gets closed (for example, we
105
+ # have a server pair and saw the "not master"
106
+ # error, which closes the connection), then
107
+ # automatically try to reconnect to the master or
108
+ # to the single server we have been given. Defaults
109
+ # to +false+.
110
+ # :logger :: Optional Logger instance to which driver usage information
111
+ # will be logged.
112
+ #
113
+ # When a DB object first connects to a pair, it will find the master
114
+ # instance and connect to that one. On socket error or if we recieve a
115
+ # "not master" error, we again find the master of the pair.
116
+ def initialize(db_name, nodes, options={})
117
+ case db_name
118
+ when Symbol, String
119
+ else
120
+ raise TypeError, "db_name must be a string or symbol"
121
+ end
122
+
123
+ [" ", ".", "$", "/", "\\"].each do |invalid_char|
124
+ if db_name.include? invalid_char
125
+ raise InvalidName, "database names cannot contain the character '#{invalid_char}'"
76
126
  end
127
+ end
128
+ if db_name.empty?
129
+ raise InvalidName, "database name cannot be the empty string"
130
+ end
131
+
132
+ @name, @nodes = db_name, nodes
133
+ @strict = options[:strict]
134
+ @pk_factory = options[:pk]
135
+ @slave_ok = options[:slave_ok] && @nodes.length == 1 # only OK if one node
136
+ @auto_reconnect = options[:auto_reconnect]
137
+ @semaphore = Object.new
138
+ @semaphore.extend Mutex_m
139
+ @socket = nil
140
+ @logger = options[:logger]
141
+ connect_to_master
142
+ end
77
143
 
78
- def connect_to_first_available_host
144
+ def connect_to_master
145
+ close if @socket
146
+ @host = @port = nil
147
+ @nodes.detect { |hp|
148
+ @host, @port = *hp
149
+ begin
150
+ @socket = TCPSocket.new(@host, @port)
151
+ @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
152
+
153
+ # Check for master. Can't call master? because it uses mutex,
154
+ # which may already be in use during this call.
155
+ semaphore_is_locked = @semaphore.locked?
156
+ @semaphore.unlock if semaphore_is_locked
157
+ is_master = master?
158
+ @semaphore.lock if semaphore_is_locked
159
+
160
+ @slave_ok || is_master
161
+ rescue SocketError, SystemCallError, IOError => ex
79
162
  close if @socket
80
- @host = @port = nil
81
- @nodes.detect { |hp|
82
- @host, @port = *hp
83
- begin
84
- @socket = TCPSocket.new(@host, @port)
85
- break if ok?(db_command(:ismaster => 1)) # success
86
- rescue => ex
87
- close if @socket
88
- end
89
- @socket
90
- }
91
- raise "error: failed to connect to any given host:port" unless @socket
163
+ false
92
164
  end
165
+ }
166
+ raise "error: failed to connect to any given host:port" unless @socket
167
+ end
93
168
 
94
- # Returns an array of collection names. Each name is of the form
95
- # "database_name.collection_name".
96
- def collection_names
97
- names = collections_info.collect { |doc| doc['name'] || '' }
98
- names.delete('')
99
- names
100
- end
169
+ # Returns true if +username+ has +password+ in
170
+ # +SYSTEM_USER_COLLECTION+. +name+ is username, +password+ is
171
+ # plaintext password.
172
+ def authenticate(username, password)
173
+ doc = db_command(:getnonce => 1)
174
+ raise "error retrieving nonce: #{doc}" unless ok?(doc)
175
+ nonce = doc['nonce']
176
+
177
+ auth = OrderedHash.new
178
+ auth['authenticate'] = 1
179
+ auth['user'] = username
180
+ auth['nonce'] = nonce
181
+ auth['key'] = Digest::MD5.hexdigest("#{nonce}#{username}#{hash_password(username, password)}")
182
+ ok?(db_command(auth))
183
+ end
101
184
 
102
- # Returns a cursor over query result hashes. Each hash contains a
103
- # 'name' string and optionally an 'options' hash. If +coll_name+ is
104
- # specified, an array of length 1 is returned.
105
- def collections_info(coll_name=nil)
106
- selector = {}
107
- selector[:name] = full_coll_name(coll_name) if coll_name
108
- query(Collection.new(self, SYSTEM_NAMESPACE_COLLECTION), Query.new(selector))
109
- end
185
+ # Deauthorizes use for this database for this connection.
186
+ def logout
187
+ doc = db_command(:logout => 1)
188
+ raise "error logging out: #{doc.inspect}" unless ok?(doc)
189
+ end
110
190
 
111
- # Create a collection. If +strict+ is false, will return existing or
112
- # new collection. If +strict+ is true, will raise an error if
113
- # collection +name+ already exists.
114
- #
115
- # Options is an optional hash:
116
- #
117
- # :capped :: Boolean. If not specified, capped is +false+.
118
- #
119
- # :size :: If +capped+ is +true+, specifies the maximum number of
120
- # bytes. If +false+, specifies the initial extent of the
121
- # collection.
122
- #
123
- # :max :: Max number of records in a capped collection. Optional.
124
- def create_collection(name, options={})
125
- # First check existence
126
- if collection_names.include?(full_coll_name(name))
127
- if strict?
128
- raise "Collection #{name} already exists. Currently in strict mode."
129
- else
130
- return Collection.new(self, name)
131
- end
132
- end
191
+ # Returns an array of collection names in this database.
192
+ def collection_names
193
+ names = collections_info.collect { |doc| doc['name'] || '' }
194
+ names = names.delete_if {|name| name.index(@name).nil? || name.index('$')}
195
+ names.map {|name| name.sub(@name + '.', '')}
196
+ end
133
197
 
134
- # Create new collection
135
- oh = OrderedHash.new
136
- oh[:create] = name
137
- doc = db_command(oh.merge(options || {}))
138
- ok = doc['ok']
139
- return Collection.new(self, name) if ok.kind_of?(Numeric) && (ok.to_i == 1 || ok.to_i == 0)
140
- raise "Error creating collection: #{doc.inspect}"
141
- end
198
+ # Retruns an array of Collection instances, one for each collection in this
199
+ # database.
200
+ def collections
201
+ collection_names.map do |collection_name|
202
+ Collection.new(self, collection_name)
203
+ end
204
+ end
142
205
 
143
- def admin
144
- Admin.new(self)
145
- end
206
+ # Returns a cursor over query result hashes. Each hash contains a
207
+ # 'name' string and optionally an 'options' hash. If +coll_name+ is
208
+ # specified, an array of length 1 is returned.
209
+ def collections_info(coll_name=nil)
210
+ selector = {}
211
+ selector[:name] = full_coll_name(coll_name) if coll_name
212
+ query(Collection.new(self, SYSTEM_NAMESPACE_COLLECTION), Query.new(selector))
213
+ end
146
214
 
147
- # Return a collection. If +strict+ is false, will return existing or
148
- # new collection. If +strict+ is true, will raise an error if
149
- # collection +name+ does not already exists.
150
- def collection(name)
151
- return Collection.new(self, name) if collection_names.include?(full_coll_name(name))
152
- if strict?
153
- raise "Collection #{name} doesn't exist. Currently in strict mode."
154
- else
155
- create_collection(name)
156
- end
215
+ # Create a collection. If +strict+ is false, will return existing or
216
+ # new collection. If +strict+ is true, will raise an error if
217
+ # collection +name+ already exists.
218
+ #
219
+ # Options is an optional hash:
220
+ #
221
+ # :capped :: Boolean. If not specified, capped is +false+.
222
+ #
223
+ # :size :: If +capped+ is +true+, specifies the maximum number of
224
+ # bytes. If +false+, specifies the initial extent of the
225
+ # collection.
226
+ #
227
+ # :max :: Max number of records in a capped collection. Optional.
228
+ def create_collection(name, options={})
229
+ # First check existence
230
+ if collection_names.include?(name)
231
+ if strict?
232
+ raise "Collection #{name} already exists. Currently in strict mode."
233
+ else
234
+ return Collection.new(self, name)
157
235
  end
236
+ end
158
237
 
159
- # Drop collection +name+. Returns +true+ on success or if the
160
- # collection does not exist, +false+ otherwise.
161
- def drop_collection(name)
162
- return true unless collection_names.include?(full_coll_name(name))
238
+ # Create new collection
239
+ oh = OrderedHash.new
240
+ oh[:create] = name
241
+ doc = db_command(oh.merge(options || {}))
242
+ ok = doc['ok']
243
+ return Collection.new(self, name) if ok.kind_of?(Numeric) && (ok.to_i == 1 || ok.to_i == 0)
244
+ raise "Error creating collection: #{doc.inspect}"
245
+ end
163
246
 
164
- coll = collection(name)
165
- coll.drop_indexes # Mongo requires that we drop indexes manually
166
- ok?(db_command(:drop => name))
167
- end
247
+ def admin
248
+ Admin.new(self)
249
+ end
168
250
 
169
- # Returns true if this database is a master (or is not paired with any
170
- # other database), false if it is a slave.
171
- def master?
172
- doc = db_command(:ismaster => 1)
173
- is_master = doc['ismaster']
174
- ok?(doc) && is_master.kind_of?(Numeric) && is_master.to_i == 1
175
- end
251
+ # Return a collection. If +strict+ is false, will return existing or
252
+ # new collection. If +strict+ is true, will raise an error if
253
+ # collection +name+ does not already exists.
254
+ def collection(name)
255
+ return Collection.new(self, name) if !strict? || collection_names.include?(name)
256
+ raise "Collection #{name} doesn't exist. Currently in strict mode."
257
+ end
258
+ alias_method :[], :collection
176
259
 
177
- # Returns a string of the form "host:port" that points to the master
178
- # database. Works even if this is the master database.
179
- def master
180
- doc = db_command(:ismaster => 1)
181
- is_master = doc['ismaster']
182
- raise "Error retrieving master database" unless ok?(doc) && is_master.kind_of?(Numeric)
183
- case is_master.to_i
184
- when 1
185
- "#@host:#@port"
186
- else
187
- doc['remote']
188
- end
189
- end
260
+ # Drop collection +name+. Returns +true+ on success or if the
261
+ # collection does not exist, +false+ otherwise.
262
+ def drop_collection(name)
263
+ return true unless collection_names.include?(name)
190
264
 
191
- # Switches our socket to the master database. If we are already the
192
- # master, no change is made.
193
- def switch_to_master
194
- master_str = master()
195
- unless master_str == "#@host:#@port"
196
- @semaphore.synchronize {
197
- master_str =~ /(.+):(\d+)/
198
- @host, @port = $1, $2
199
- close()
200
- @socket = TCPSocket.new(@host, @port)
201
- }
202
- end
203
- end
265
+ ok?(db_command(:drop => name))
266
+ end
204
267
 
205
- # Close the connection to the database.
206
- def close
207
- @socket.close if @socket
208
- @socket = nil
209
- end
268
+ # Returns the error message from the most recently executed database
269
+ # operation for this connection, or +nil+ if there was no error.
270
+ def error
271
+ doc = db_command(:getlasterror => 1)
272
+ raise "error retrieving last error: #{doc}" unless ok?(doc)
273
+ doc['err']
274
+ end
210
275
 
211
- def connected?
212
- @socket != nil
213
- end
276
+ # Get status information from the last operation on this connection.
277
+ def last_status
278
+ db_command(:getlasterror => 1)
279
+ end
214
280
 
215
- # Send a MsgMessage to the database.
216
- def send_message(msg)
217
- send_to_db(MsgMessage.new(msg))
218
- end
219
-
220
- # Returns a Cursor over the query results.
221
- #
222
- # Note that the query gets sent lazily; the cursor calls
223
- # #send_query_message when needed. If the caller never requests an
224
- # object from the cursor, the query never gets sent.
225
- def query(collection, query)
226
- Cursor.new(self, collection, query)
227
- end
281
+ # Returns +true+ if an error was caused by the most recently executed
282
+ # database operation.
283
+ def error?
284
+ error != nil
285
+ end
228
286
 
229
- # Used by a Cursor to lazily send the query to the database.
230
- def send_query_message(query_message)
231
- @semaphore.synchronize {
232
- send_to_db(query_message)
233
- }
234
- end
287
+ # Get the most recent error to have occured on this database
288
+ #
289
+ # Only returns errors that have occured since the last call to
290
+ # DB#reset_error_history - returns +nil+ if there is no such error.
291
+ def previous_error
292
+ error = db_command(:getpreverror => 1)
293
+ if error["err"]
294
+ error
295
+ else
296
+ nil
297
+ end
298
+ end
235
299
 
236
- # Remove the records that match +selector+ from +collection_name+.
237
- # Normally called by Collection#remove or Collection#clear.
238
- def remove_from_db(collection_name, selector)
239
- @semaphore.synchronize {
240
- send_to_db(RemoveMessage.new(@name, collection_name, selector))
241
- }
242
- end
300
+ # Reset the error history of this database
301
+ #
302
+ # Calls to DB#previous_error will only return errors that have occurred
303
+ # since the most recent call to this method.
304
+ def reset_error_history
305
+ db_command(:reseterror => 1)
306
+ end
243
307
 
244
- # Update records in +collection_name+ that match +selector+ by
245
- # applying +obj+ as an update. Normally called by Collection#replace.
246
- def replace_in_db(collection_name, selector, obj)
247
- @semaphore.synchronize {
248
- send_to_db(UpdateMessage.new(@name, collection_name, selector, obj, false))
249
- }
250
- end
308
+ # Returns true if this database is a master (or is not paired with any
309
+ # other database), false if it is a slave.
310
+ def master?
311
+ doc = db_command(:ismaster => 1)
312
+ is_master = doc['ismaster']
313
+ ok?(doc) && is_master.kind_of?(Numeric) && is_master.to_i == 1
314
+ end
251
315
 
252
- # Alias for #replace_in_db. Normally called by Collection.modify.
253
- alias_method :modify_in_db, :replace_in_db
254
-
255
- # Update records in +collection_name+ that match +selector+ by
256
- # applying +obj+ as an update. If no match, inserts (???). Normally
257
- # called by Collection#repsert.
258
- def repsert_in_db(collection_name, selector, obj)
259
- # TODO if PKInjector, inject
260
- @semaphore.synchronize {
261
- send_to_db(UpdateMessage.new(@name, collection_name, selector, obj, true))
262
- obj
263
- }
264
- end
316
+ # Returns a string of the form "host:port" that points to the master
317
+ # database. Works even if this is the master database.
318
+ def master
319
+ doc = db_command(:ismaster => 1)
320
+ is_master = doc['ismaster']
321
+ raise "Error retrieving master database: #{doc.inspect}" unless ok?(doc) && is_master.kind_of?(Numeric)
322
+ case is_master.to_i
323
+ when 1
324
+ "#@host:#@port"
325
+ else
326
+ doc['remote']
327
+ end
328
+ end
265
329
 
266
- # Return the number of records in +collection_name+ that match
267
- # +selector+. If +selector+ is +nil+ or an empty hash, returns the
268
- # count of all records. Normally called by Collection#count.
269
- def count(collection_name, selector={})
270
- oh = OrderedHash.new
271
- oh[:count] = collection_name
272
- oh[:query] = selector || {}
273
- doc = db_command(oh)
274
- return doc['n'].to_i if ok?(doc)
275
- raise "Error with count command: #{doc.inspect}"
276
- end
330
+ # Close the connection to the database.
331
+ def close
332
+ if @socket
333
+ s = @socket
334
+ @socket = nil
335
+ s.close
336
+ end
337
+ end
277
338
 
278
- # Drop index +name+ from +collection_name+. Normally called from
279
- # Collection#drop_index or Collection#drop_indexes.
280
- def drop_index(collection_name, name)
281
- oh = OrderedHash.new
282
- oh[:deleteIndexes] = collection_name
283
- oh[:index] = name
284
- doc = db_command(oh)
285
- raise "Error with drop_index command: #{doc.inspect}" unless ok?(doc)
286
- end
339
+ def connected?
340
+ @socket != nil
341
+ end
287
342
 
288
- # Return an array of hashes, one for each index on +collection_name+.
289
- # Normally called by Collection#index_information. Each hash contains:
290
- #
291
- # :name :: Index name
292
- #
293
- # :keys :: Hash whose keys are the names of the fields that make up
294
- # the key and values are integers.
295
- #
296
- # :ns :: Namespace; same as +collection_name+.
297
- def index_information(collection_name)
298
- sel = {:ns => full_coll_name(collection_name)}
299
- query(Collection.new(self, SYSTEM_INDEX_COLLECTION), Query.new(sel)).collect { |row|
300
- h = {:name => row['name']}
301
- raise "Name of index on return from db was nil. Coll = #{full_coll_name(collection_name)}" unless h[:name]
302
-
303
- h[:keys] = row['key']
304
- raise "Keys for index on return from db was nil. Coll = #{full_coll_name(collection_name)}" unless h[:keys]
305
-
306
- h[:ns] = row['ns']
307
- raise "Namespace for index on return from db was nil. Coll = #{full_coll_name(collection_name)}" unless h[:ns]
308
- h[:ns].sub!(/.*\./, '')
309
- raise "Error: ns != collection" unless h[:ns] == collection_name
310
-
311
- h
312
- }
313
- end
343
+ def receive_full(length)
344
+ message = ""
345
+ while message.length < length do
346
+ chunk = @socket.recv(length - message.length)
347
+ raise "connection closed" unless chunk.length > 0
348
+ message += chunk
349
+ end
350
+ message
351
+ end
314
352
 
315
- # Create a new index on +collection_name+ named +index_name+. +fields+
316
- # should be an array of field names. Normally called by
317
- # Collection#create_index.
318
- def create_index(collection_name, index_name, fields)
319
- sel = {:name => index_name, :ns => full_coll_name(collection_name)}
320
- field_h = {}
321
- fields.each { |f| field_h[f] = 1 }
322
- sel[:key] = field_h
323
- @semaphore.synchronize {
324
- send_to_db(InsertMessage.new(@name, SYSTEM_INDEX_COLLECTION, sel))
325
- }
326
- end
353
+ # Send a MsgMessage to the database.
354
+ def send_message(msg)
355
+ send_to_db(MsgMessage.new(msg))
356
+ end
357
+
358
+ # Returns a Cursor over the query results.
359
+ #
360
+ # Note that the query gets sent lazily; the cursor calls
361
+ # #send_query_message when needed. If the caller never requests an
362
+ # object from the cursor, the query never gets sent.
363
+ def query(collection, query, admin=false)
364
+ Cursor.new(self, collection, query, admin)
365
+ end
366
+
367
+ # Used by a Cursor to lazily send the query to the database.
368
+ def send_query_message(query_message)
369
+ send_to_db(query_message)
370
+ end
371
+
372
+ # Remove the records that match +selector+ from +collection_name+.
373
+ # Normally called by Collection#remove or Collection#clear.
374
+ def remove_from_db(collection_name, selector)
375
+ _synchronize {
376
+ send_to_db(RemoveMessage.new(@name, collection_name, selector))
377
+ }
378
+ end
379
+
380
+ # Update records in +collection_name+ that match +selector+ by
381
+ # applying +obj+ as an update. Normally called by Collection#replace.
382
+ def replace_in_db(collection_name, selector, obj)
383
+ _synchronize {
384
+ send_to_db(UpdateMessage.new(@name, collection_name, selector, obj, false))
385
+ }
386
+ end
387
+
388
+ # Update records in +collection_name+ that match +selector+ by
389
+ # applying +obj+ as an update. If no match, inserts (???). Normally
390
+ # called by Collection#repsert.
391
+ def repsert_in_db(collection_name, selector, obj)
392
+ _synchronize {
393
+ obj = @pk_factory.create_pk(obj) if @pk_factory
394
+ send_to_db(UpdateMessage.new(@name, collection_name, selector, obj, true))
395
+ obj
396
+ }
397
+ end
398
+
399
+ # Dereference a DBRef, getting the document it points to.
400
+ def dereference(dbref)
401
+ collection(dbref.namespace).find_one("_id" => dbref.object_id)
402
+ end
403
+
404
+ # Evaluate a JavaScript expression on MongoDB.
405
+ # +code+ should be a string or Code instance containing a JavaScript
406
+ # expression. Additional arguments will be passed to that expression
407
+ # when it is run on the server.
408
+ def eval(code, *args)
409
+ if not code.is_a? Code
410
+ code = Code.new(code)
411
+ end
412
+
413
+ oh = OrderedHash.new
414
+ oh[:$eval] = code
415
+ oh[:args] = args
416
+ doc = db_command(oh)
417
+ return doc['retval'] if ok?(doc)
418
+ raise OperationFailure, "eval failed: #{doc['errmsg']}"
419
+ end
420
+
421
+ # Rename collection +from+ to +to+. Meant to be called by
422
+ # Collection#rename.
423
+ def rename_collection(from, to)
424
+ oh = OrderedHash.new
425
+ oh[:renameCollection] = "#{@name}.#{from}"
426
+ oh[:to] = "#{@name}.#{to}"
427
+ doc = db_command(oh, true)
428
+ raise "Error renaming collection: #{doc.inspect}" unless ok?(doc)
429
+ end
430
+
431
+ # Drop index +name+ from +collection_name+. Normally called from
432
+ # Collection#drop_index or Collection#drop_indexes.
433
+ def drop_index(collection_name, name)
434
+ oh = OrderedHash.new
435
+ oh[:deleteIndexes] = collection_name
436
+ oh[:index] = name
437
+ doc = db_command(oh)
438
+ raise "Error with drop_index command: #{doc.inspect}" unless ok?(doc)
439
+ end
327
440
 
328
- # Insert +objects+ into +collection_name+. Normally called by
329
- # Collection#insert.
330
- def insert_into_db(collection_name, objects)
331
- @semaphore.synchronize {
332
- objects.each { |o| send_to_db(InsertMessage.new(@name, collection_name, o)) }
441
+ # Get information on the indexes for the collection +collection_name+.
442
+ # Normally called by Collection#index_information. Returns a hash where
443
+ # the keys are index names (as returned by Collection#create_index and
444
+ # the values are lists of [key, direction] pairs specifying the index
445
+ # (as passed to Collection#create_index).
446
+ def index_information(collection_name)
447
+ sel = {:ns => full_coll_name(collection_name)}
448
+ info = {}
449
+ query(Collection.new(self, SYSTEM_INDEX_COLLECTION), Query.new(sel)).each { |index|
450
+ info[index['name']] = index['key'].to_a
451
+ }
452
+ info
453
+ end
454
+
455
+ # Create a new index on +collection_name+. +field_or_spec+
456
+ # should be either a single field name or a Array of [field name,
457
+ # direction] pairs. Directions should be specified as
458
+ # Mongo::ASCENDING or Mongo::DESCENDING. Normally called
459
+ # by Collection#create_index. If +unique+ is true the index will
460
+ # enforce a uniqueness constraint.
461
+ def create_index(collection_name, field_or_spec, unique=false)
462
+ field_h = OrderedHash.new
463
+ if field_or_spec.is_a?(String) || field_or_spec.is_a?(Symbol)
464
+ field_h[field_or_spec.to_s] = 1
465
+ else
466
+ field_or_spec.each { |f| field_h[f[0].to_s] = f[1] }
467
+ end
468
+ name = gen_index_name(field_h)
469
+ sel = {
470
+ :name => name,
471
+ :ns => full_coll_name(collection_name),
472
+ :key => field_h,
473
+ :unique => unique
474
+ }
475
+ _synchronize {
476
+ send_to_db(InsertMessage.new(@name, SYSTEM_INDEX_COLLECTION, false, sel))
477
+ }
478
+ name
479
+ end
480
+
481
+ # Insert +objects+ into +collection_name+. Normally called by
482
+ # Collection#insert. Returns a new array containing the _ids
483
+ # of the inserted documents.
484
+ def insert_into_db(collection_name, objects)
485
+ _synchronize {
486
+ if @pk_factory
487
+ objects.collect! { |o|
488
+ @pk_factory.create_pk(o)
333
489
  }
490
+ else
491
+ objects = objects.collect do |o|
492
+ o[:_id] || o['_id'] ? o : o.merge!(:_id => ObjectID.new)
493
+ end
334
494
  end
495
+ send_to_db(InsertMessage.new(@name, collection_name, true, *objects))
496
+ objects.collect { |o| o[:_id] || o['_id'] }
497
+ }
498
+ end
335
499
 
336
- def send_to_db(message)
337
- @socket.print(message.buf.to_s)
338
- end
500
+ def send_to_db(message)
501
+ connect_to_master if !connected? && @auto_reconnect
502
+ begin
503
+ @logger.debug(" MONGODB #{message}") if @logger
504
+ @socket.print(message.buf.to_s)
505
+ @socket.flush
506
+ rescue => ex
507
+ close
508
+ raise ex
509
+ end
510
+ end
339
511
 
340
- def full_coll_name(collection_name)
341
- "#{@name}.#{collection_name}"
342
- end
512
+ def full_coll_name(collection_name)
513
+ "#{@name}.#{collection_name}"
514
+ end
343
515
 
344
- # Return +true+ if +doc+ contains an 'ok' field with the value 1.
345
- def ok?(doc)
346
- ok = doc['ok']
347
- ok.kind_of?(Numeric) && ok.to_i == 1
516
+ # Return +true+ if +doc+ contains an 'ok' field with the value 1.
517
+ def ok?(doc)
518
+ ok = doc['ok']
519
+ ok.kind_of?(Numeric) && ok.to_i == 1
520
+ end
521
+
522
+ # DB commands need to be ordered, so selector must be an OrderedHash
523
+ # (or a Hash with only one element). What DB commands really need is
524
+ # that the "command" key be first.
525
+ def db_command(selector, use_admin_db=false)
526
+ if !selector.kind_of?(OrderedHash)
527
+ if !selector.kind_of?(Hash) || selector.keys.length > 1
528
+ raise "db_command must be given an OrderedHash when there is more than one key"
348
529
  end
530
+ end
349
531
 
350
- # DB commands need to be ordered, so selector must be an OrderedHash
351
- # (or a Hash with only one element). What DB commands really need is
352
- # that the "command" key be first.
353
- #
354
- # Do not call this. Intended for driver use only.
355
- def db_command(selector)
356
- if !selector.kind_of?(OrderedHash)
357
- if !selector.kind_of?(Hash) || selector.keys.length > 1
358
- raise "db_command must be given an OrderedHash when there is more than one key"
359
- end
360
- end
532
+ q = Query.new(selector)
533
+ q.number_to_return = -1
534
+ query(Collection.new(self, SYSTEM_COMMAND_COLLECTION), q, use_admin_db).next_object
535
+ end
361
536
 
362
- q = Query.new(selector)
363
- q.number_to_return = 1
364
- query(Collection.new(self, SYSTEM_COMMAND_COLLECTION), q).next_object
365
- end
537
+ def _synchronize &block
538
+ @semaphore.synchronize &block
539
+ end
366
540
 
367
- end
541
+ private
542
+
543
+ def hash_password(username, plaintext)
544
+ Digest::MD5.hexdigest("#{username}:mongo:#{plaintext}")
545
+ end
546
+
547
+ def gen_index_name(spec)
548
+ temp = []
549
+ spec.each_pair { |field, direction|
550
+ temp = temp.push("#{field}_#{direction}")
551
+ }
552
+ return temp.join("_")
368
553
  end
369
554
  end
370
555
  end