pahagon-mongo-abd 0.14.1
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.
- data/README.rdoc +353 -0
- data/Rakefile +62 -0
- data/bin/bson_benchmark.rb +59 -0
- data/bin/mongo_console +21 -0
- data/bin/run_test_script +19 -0
- data/bin/standard_benchmark +109 -0
- data/examples/admin.rb +41 -0
- data/examples/benchmarks.rb +42 -0
- data/examples/blog.rb +76 -0
- data/examples/capped.rb +23 -0
- data/examples/cursor.rb +47 -0
- data/examples/gridfs.rb +87 -0
- data/examples/index_test.rb +125 -0
- data/examples/info.rb +30 -0
- data/examples/queries.rb +69 -0
- data/examples/simple.rb +23 -0
- data/examples/strict.rb +34 -0
- data/examples/types.rb +35 -0
- data/lib/mongo.rb +19 -0
- data/lib/mongo/admin.rb +83 -0
- data/lib/mongo/collection.rb +415 -0
- data/lib/mongo/connection.rb +151 -0
- data/lib/mongo/cursor.rb +279 -0
- data/lib/mongo/db.rb +560 -0
- data/lib/mongo/errors.rb +26 -0
- data/lib/mongo/gridfs.rb +16 -0
- data/lib/mongo/gridfs/chunk.rb +92 -0
- data/lib/mongo/gridfs/grid_store.rb +464 -0
- data/lib/mongo/message.rb +20 -0
- data/lib/mongo/message/get_more_message.rb +32 -0
- data/lib/mongo/message/insert_message.rb +37 -0
- data/lib/mongo/message/kill_cursors_message.rb +31 -0
- data/lib/mongo/message/message.rb +80 -0
- data/lib/mongo/message/message_header.rb +45 -0
- data/lib/mongo/message/msg_message.rb +29 -0
- data/lib/mongo/message/opcodes.rb +27 -0
- data/lib/mongo/message/query_message.rb +78 -0
- data/lib/mongo/message/remove_message.rb +37 -0
- data/lib/mongo/message/update_message.rb +38 -0
- data/lib/mongo/query.rb +118 -0
- data/lib/mongo/types/binary.rb +38 -0
- data/lib/mongo/types/code.rb +30 -0
- data/lib/mongo/types/dbref.rb +33 -0
- data/lib/mongo/types/objectid.rb +143 -0
- data/lib/mongo/types/regexp_of_holding.rb +40 -0
- data/lib/mongo/util/bson.rb +546 -0
- data/lib/mongo/util/byte_buffer.rb +167 -0
- data/lib/mongo/util/ordered_hash.rb +113 -0
- data/lib/mongo/util/xml_to_ruby.rb +105 -0
- data/mongo-ruby-driver.gemspec +103 -0
- data/test/mongo-qa/_common.rb +8 -0
- data/test/mongo-qa/admin +26 -0
- data/test/mongo-qa/capped +22 -0
- data/test/mongo-qa/count1 +18 -0
- data/test/mongo-qa/dbs +22 -0
- data/test/mongo-qa/find +10 -0
- data/test/mongo-qa/find1 +15 -0
- data/test/mongo-qa/gridfs_in +16 -0
- data/test/mongo-qa/gridfs_out +17 -0
- data/test/mongo-qa/indices +49 -0
- data/test/mongo-qa/remove +25 -0
- data/test/mongo-qa/stress1 +35 -0
- data/test/mongo-qa/test1 +11 -0
- data/test/mongo-qa/update +18 -0
- data/test/test_admin.rb +69 -0
- data/test/test_bson.rb +268 -0
- data/test/test_byte_buffer.rb +69 -0
- data/test/test_chunk.rb +84 -0
- data/test/test_collection.rb +249 -0
- data/test/test_connection.rb +101 -0
- data/test/test_cursor.rb +331 -0
- data/test/test_db.rb +185 -0
- data/test/test_db_api.rb +798 -0
- data/test/test_db_connection.rb +18 -0
- data/test/test_grid_store.rb +284 -0
- data/test/test_message.rb +35 -0
- data/test/test_objectid.rb +105 -0
- data/test/test_ordered_hash.rb +138 -0
- data/test/test_round_trip.rb +120 -0
- data/test/test_threading.rb +37 -0
- metadata +135 -0
@@ -0,0 +1,151 @@
|
|
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
|
+
require 'mongo/db'
|
18
|
+
|
19
|
+
module Mongo
|
20
|
+
|
21
|
+
# A connection to MongoDB.
|
22
|
+
class Connection
|
23
|
+
|
24
|
+
DEFAULT_PORT = 27017
|
25
|
+
|
26
|
+
# Create a Mongo database server instance. You specify either one or a
|
27
|
+
# pair of servers. If one, you also say if connecting to a slave is
|
28
|
+
# OK. In either case, the host default is "localhost" and port default
|
29
|
+
# is DEFAULT_PORT.
|
30
|
+
#
|
31
|
+
# If you specify a pair, pair_or_host is a hash with two keys :left
|
32
|
+
# and :right. Each key maps to either
|
33
|
+
# * a server name, in which case port is DEFAULT_PORT
|
34
|
+
# * a port number, in which case server is "localhost"
|
35
|
+
# * an array containing a server name and a port number in that order
|
36
|
+
#
|
37
|
+
# +options+ are passed on to each DB instance:
|
38
|
+
#
|
39
|
+
# :slave_ok :: Only used if one host is specified. If false, when
|
40
|
+
# connecting to that host/port a DB object will check to
|
41
|
+
# see if the server is the master. If it is not, an error
|
42
|
+
# is thrown.
|
43
|
+
#
|
44
|
+
# :auto_reconnect :: If a DB connection gets closed (for example, we
|
45
|
+
# have a server pair and saw the "not master"
|
46
|
+
# error, which closes the connection), then
|
47
|
+
# automatically try to reconnect to the master or
|
48
|
+
# to the single server we have been given. Defaults
|
49
|
+
# to +false+.
|
50
|
+
# :logger :: Optional Logger instance to which driver usage information
|
51
|
+
# will be logged.
|
52
|
+
#
|
53
|
+
# Since that's so confusing, here are a few examples:
|
54
|
+
#
|
55
|
+
# Connection.new # localhost, DEFAULT_PORT, !slave
|
56
|
+
# Connection.new("localhost") # localhost, DEFAULT_PORT, !slave
|
57
|
+
# Connection.new("localhost", 3000) # localhost, 3000, slave not ok
|
58
|
+
# # localhost, 3000, slave ok
|
59
|
+
# Connection.new("localhost", 3000, :slave_ok => true)
|
60
|
+
# # localhost, DEFAULT_PORT, auto reconnect
|
61
|
+
# Connection.new(nil, nil, :auto_reconnect => true)
|
62
|
+
#
|
63
|
+
# # A pair of servers. DB will always talk to the master. On socket
|
64
|
+
# # error or "not master" error, we will auto-reconnect to the
|
65
|
+
# # current master.
|
66
|
+
# Connection.new({:left => ["db1.example.com", 3000],
|
67
|
+
# :right => "db2.example.com"}, # DEFAULT_PORT
|
68
|
+
# nil, :auto_reconnect => true)
|
69
|
+
#
|
70
|
+
# # Here, :right is localhost/DEFAULT_PORT. No auto-reconnect.
|
71
|
+
# Connection.new({:left => ["db1.example.com", 3000]})
|
72
|
+
#
|
73
|
+
# When a DB object first connects to a pair, it will find the master
|
74
|
+
# instance and connect to that one.
|
75
|
+
def initialize(pair_or_host=nil, port=nil, options={})
|
76
|
+
@pair = case pair_or_host
|
77
|
+
when String
|
78
|
+
[[pair_or_host, port ? port.to_i : DEFAULT_PORT]]
|
79
|
+
when Hash
|
80
|
+
connections = []
|
81
|
+
connections << pair_val_to_connection(pair_or_host[:left])
|
82
|
+
connections << pair_val_to_connection(pair_or_host[:right])
|
83
|
+
connections
|
84
|
+
when nil
|
85
|
+
[['localhost', DEFAULT_PORT]]
|
86
|
+
end
|
87
|
+
|
88
|
+
@options = options
|
89
|
+
end
|
90
|
+
|
91
|
+
# Return the Mongo::DB named +db_name+. The slave_ok and
|
92
|
+
# auto_reconnect options passed in via #new may be overridden here.
|
93
|
+
# See DB#new for other options you can pass in.
|
94
|
+
def db(db_name, options={})
|
95
|
+
DB.new(db_name, @pair, @options.merge(options))
|
96
|
+
end
|
97
|
+
|
98
|
+
# Returns a hash containing database names as keys and disk space for
|
99
|
+
# each as values.
|
100
|
+
def database_info
|
101
|
+
doc = single_db_command('admin', :listDatabases => 1)
|
102
|
+
h = {}
|
103
|
+
doc['databases'].each { |db|
|
104
|
+
h[db['name']] = db['sizeOnDisk'].to_i
|
105
|
+
}
|
106
|
+
h
|
107
|
+
end
|
108
|
+
|
109
|
+
# Returns an array of database names.
|
110
|
+
def database_names
|
111
|
+
database_info.keys
|
112
|
+
end
|
113
|
+
|
114
|
+
# Drops the database +name+.
|
115
|
+
def drop_database(name)
|
116
|
+
single_db_command(name, :dropDatabase => 1)
|
117
|
+
end
|
118
|
+
|
119
|
+
protected
|
120
|
+
|
121
|
+
# Turns an array containing a host name string and a
|
122
|
+
# port number integer into a [host, port] pair array.
|
123
|
+
def pair_val_to_connection(a)
|
124
|
+
case a
|
125
|
+
when nil
|
126
|
+
['localhost', DEFAULT_PORT]
|
127
|
+
when String
|
128
|
+
[a, DEFAULT_PORT]
|
129
|
+
when Integer
|
130
|
+
['localhost', a]
|
131
|
+
when Array
|
132
|
+
a
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
# Send cmd (a hash, possibly ordered) to the admin database and return
|
137
|
+
# the answer. Raises an error unless the return is "ok" (DB#ok?
|
138
|
+
# returns +true+).
|
139
|
+
def single_db_command(db_name, cmd)
|
140
|
+
db = nil
|
141
|
+
begin
|
142
|
+
db = db(db_name)
|
143
|
+
doc = db.db_command(cmd)
|
144
|
+
raise "error retrieving database info: #{doc.inspect}" unless db.ok?(doc)
|
145
|
+
doc
|
146
|
+
ensure
|
147
|
+
db.close if db
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
data/lib/mongo/cursor.rb
ADDED
@@ -0,0 +1,279 @@
|
|
1
|
+
# Copyright (C) 2008-2009 10gen Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
require 'mongo/message'
|
16
|
+
require 'mongo/util/byte_buffer'
|
17
|
+
require 'mongo/util/bson'
|
18
|
+
|
19
|
+
module Mongo
|
20
|
+
|
21
|
+
# A cursor over query results. Returned objects are hashes.
|
22
|
+
class Cursor
|
23
|
+
|
24
|
+
include Enumerable
|
25
|
+
|
26
|
+
RESPONSE_HEADER_SIZE = 20
|
27
|
+
|
28
|
+
attr_reader :db, :collection, :query
|
29
|
+
|
30
|
+
# Create a new cursor.
|
31
|
+
#
|
32
|
+
# Should not be called directly by application developers.
|
33
|
+
def initialize(db, collection, query, admin=false)
|
34
|
+
@db, @collection, @query, @admin = db, collection, query, admin
|
35
|
+
@cache = []
|
36
|
+
@closed = false
|
37
|
+
@query_run = false
|
38
|
+
end
|
39
|
+
|
40
|
+
# Return the next object or nil if there are no more. Raises an error
|
41
|
+
# if necessary.
|
42
|
+
def next_object
|
43
|
+
refill_via_get_more if num_remaining == 0
|
44
|
+
o = @cache.shift
|
45
|
+
|
46
|
+
if o && o['$err']
|
47
|
+
err = o['$err']
|
48
|
+
|
49
|
+
# If the server has stopped being the master (e.g., it's one of a
|
50
|
+
# pair but it has died or something like that) then we close that
|
51
|
+
# connection. If the db has auto connect option and a pair of
|
52
|
+
# servers, next request will re-open on master server.
|
53
|
+
@db.close if err == "not master"
|
54
|
+
|
55
|
+
raise err
|
56
|
+
end
|
57
|
+
|
58
|
+
o
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get the size of the results set for this query.
|
62
|
+
#
|
63
|
+
# Returns the number of objects in the results set for this query. Does
|
64
|
+
# not take limit and skip into account. Raises OperationFailure on a
|
65
|
+
# database error.
|
66
|
+
def count
|
67
|
+
command = OrderedHash["count", @collection.name,
|
68
|
+
"query", @query.selector,
|
69
|
+
"fields", @query.fields()]
|
70
|
+
response = @db.db_command(command)
|
71
|
+
return response['n'].to_i if response['ok'] == 1
|
72
|
+
return 0 if response['errmsg'] == "ns missing"
|
73
|
+
raise OperationFailure, "Count failed: #{response['errmsg']}"
|
74
|
+
end
|
75
|
+
|
76
|
+
# Sort this cursor's result
|
77
|
+
#
|
78
|
+
# Takes either a hash of field names as keys and 1/-1 as values; 1 ==
|
79
|
+
# ascending, -1 == descending, or array of field names (all assumed to be
|
80
|
+
# sorted in ascending order).
|
81
|
+
#
|
82
|
+
# Raises InvalidOperation if this cursor has already been used.
|
83
|
+
#
|
84
|
+
# This method overrides any sort order specified in the Collection#find
|
85
|
+
# method, and only the last sort applied has an effect
|
86
|
+
def sort(order)
|
87
|
+
check_modifiable
|
88
|
+
@query.order_by = order
|
89
|
+
self
|
90
|
+
end
|
91
|
+
|
92
|
+
# Limits the number of results to be returned by this cursor.
|
93
|
+
#
|
94
|
+
# Raises InvalidOperation if this cursor has already been used.
|
95
|
+
#
|
96
|
+
# This method overrides any limit specified in the Collection#find method,
|
97
|
+
# and only the last limit applied has an effect.
|
98
|
+
def limit(number_to_return)
|
99
|
+
check_modifiable
|
100
|
+
raise ArgumentError, "limit requires an integer" unless number_to_return.is_a? Integer
|
101
|
+
|
102
|
+
@query.number_to_return = number_to_return
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
106
|
+
# Skips the first +number_to_skip+ results of this cursor.
|
107
|
+
#
|
108
|
+
# Raises InvalidOperation if this cursor has already been used.
|
109
|
+
#
|
110
|
+
# This method overrides any skip specified in the Collection#find method,
|
111
|
+
# and only the last skip applied has an effect.
|
112
|
+
def skip(number_to_skip)
|
113
|
+
check_modifiable
|
114
|
+
raise ArgumentError, "skip requires an integer" unless number_to_skip.is_a? Integer
|
115
|
+
|
116
|
+
@query.number_to_skip = number_to_skip
|
117
|
+
self
|
118
|
+
end
|
119
|
+
|
120
|
+
# Iterate over each document in this cursor, yielding it to the given
|
121
|
+
# block.
|
122
|
+
#
|
123
|
+
# Iterating over an entire cursor will close it.
|
124
|
+
def each
|
125
|
+
num_returned = 0
|
126
|
+
while more? && (@query.number_to_return <= 0 || num_returned < @query.number_to_return)
|
127
|
+
yield next_object()
|
128
|
+
num_returned += 1
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# Return all of the documents in this cursor as an array of hashes.
|
133
|
+
#
|
134
|
+
# Raises InvalidOperation if this cursor has already been used (including
|
135
|
+
# any previous calls to this method).
|
136
|
+
#
|
137
|
+
# Use of this method is discouraged - iterating over a cursor is much
|
138
|
+
# more efficient in most cases.
|
139
|
+
def to_a
|
140
|
+
raise InvalidOperation, "can't call Cursor#to_a on a used cursor" if @query_run
|
141
|
+
rows = []
|
142
|
+
num_returned = 0
|
143
|
+
while more? && (@query.number_to_return <= 0 || num_returned < @query.number_to_return)
|
144
|
+
rows << next_object()
|
145
|
+
num_returned += 1
|
146
|
+
end
|
147
|
+
rows
|
148
|
+
end
|
149
|
+
|
150
|
+
# Returns an explain plan record for this cursor.
|
151
|
+
def explain
|
152
|
+
limit = @query.number_to_return
|
153
|
+
@query.explain = true
|
154
|
+
@query.number_to_return = -limit.abs
|
155
|
+
|
156
|
+
c = Cursor.new(@db, @collection, @query)
|
157
|
+
explanation = c.next_object
|
158
|
+
c.close
|
159
|
+
|
160
|
+
@query.explain = false
|
161
|
+
@query.number_to_return = limit
|
162
|
+
explanation
|
163
|
+
end
|
164
|
+
|
165
|
+
# Close the cursor.
|
166
|
+
#
|
167
|
+
# Note: if a cursor is read until exhausted (read until OP_QUERY or
|
168
|
+
# OP_GETMORE returns zero for the cursor id), there is no need to
|
169
|
+
# close it by calling this method.
|
170
|
+
#
|
171
|
+
# Collection#find takes an optional block argument which can be used to
|
172
|
+
# ensure that your cursors get closed. See the documentation for
|
173
|
+
# Collection#find for details.
|
174
|
+
def close
|
175
|
+
@db.send_to_db(KillCursorsMessage.new(@cursor_id)) if @cursor_id
|
176
|
+
@cache = []
|
177
|
+
@cursor_id = 0
|
178
|
+
@closed = true
|
179
|
+
end
|
180
|
+
|
181
|
+
# Returns true if this cursor is closed, false otherwise.
|
182
|
+
def closed?; @closed; end
|
183
|
+
|
184
|
+
private
|
185
|
+
|
186
|
+
def read_all
|
187
|
+
read_message_header
|
188
|
+
read_response_header
|
189
|
+
read_objects_off_wire
|
190
|
+
end
|
191
|
+
|
192
|
+
def read_objects_off_wire
|
193
|
+
while doc = next_object_on_wire
|
194
|
+
@cache << doc
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def read_message_header
|
199
|
+
MessageHeader.new.read_header(@db)
|
200
|
+
end
|
201
|
+
|
202
|
+
def read_response_header
|
203
|
+
header_buf = ByteBuffer.new
|
204
|
+
header_buf.put_array(@db.receive_full(RESPONSE_HEADER_SIZE).unpack("C*"))
|
205
|
+
raise "Short read for DB response header; expected #{RESPONSE_HEADER_SIZE} bytes, saw #{header_buf.length}" unless header_buf.length == RESPONSE_HEADER_SIZE
|
206
|
+
header_buf.rewind
|
207
|
+
@result_flags = header_buf.get_int
|
208
|
+
@cursor_id = header_buf.get_long
|
209
|
+
@starting_from = header_buf.get_int
|
210
|
+
@n_returned = header_buf.get_int
|
211
|
+
@n_remaining = @n_returned
|
212
|
+
end
|
213
|
+
|
214
|
+
def num_remaining
|
215
|
+
refill_via_get_more if @cache.length == 0
|
216
|
+
@cache.length
|
217
|
+
end
|
218
|
+
|
219
|
+
# Internal method, not for general use. Return +true+ if there are
|
220
|
+
# more records to retrieve. We do not check @query.number_to_return;
|
221
|
+
# #each is responsible for doing that.
|
222
|
+
def more?
|
223
|
+
num_remaining > 0
|
224
|
+
end
|
225
|
+
|
226
|
+
def next_object_on_wire
|
227
|
+
# if @n_remaining is 0 but we have a non-zero cursor, there are more
|
228
|
+
# to fetch, so do a GetMore operation, but don't do it here - do it
|
229
|
+
# when someone pulls an object out of the cache and it's empty
|
230
|
+
return nil if @n_remaining == 0
|
231
|
+
object_from_stream
|
232
|
+
end
|
233
|
+
|
234
|
+
def refill_via_get_more
|
235
|
+
if send_query_if_needed or @cursor_id == 0
|
236
|
+
return
|
237
|
+
end
|
238
|
+
@db._synchronize {
|
239
|
+
@db.send_to_db(GetMoreMessage.new(@admin ? 'admin' : @db.name, @collection.name, @cursor_id))
|
240
|
+
read_all
|
241
|
+
}
|
242
|
+
end
|
243
|
+
|
244
|
+
def object_from_stream
|
245
|
+
buf = ByteBuffer.new
|
246
|
+
buf.put_array(@db.receive_full(4).unpack("C*"))
|
247
|
+
buf.rewind
|
248
|
+
size = buf.get_int
|
249
|
+
buf.put_array(@db.receive_full(size - 4).unpack("C*"), 4)
|
250
|
+
@n_remaining -= 1
|
251
|
+
buf.rewind
|
252
|
+
BSON.new.deserialize(buf)
|
253
|
+
end
|
254
|
+
|
255
|
+
def send_query_if_needed
|
256
|
+
# Run query first time we request an object from the wire
|
257
|
+
if @query_run
|
258
|
+
false
|
259
|
+
else
|
260
|
+
@db._synchronize {
|
261
|
+
@db.send_query_message(QueryMessage.new(@admin ? 'admin' : @db.name, @collection.name, @query))
|
262
|
+
@query_run = true
|
263
|
+
read_all
|
264
|
+
}
|
265
|
+
true
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
def to_s
|
270
|
+
"DBResponse(flags=#@result_flags, cursor_id=#@cursor_id, start=#@starting_from, n_returned=#@n_returned)"
|
271
|
+
end
|
272
|
+
|
273
|
+
def check_modifiable
|
274
|
+
if @query_run || @closed
|
275
|
+
raise InvalidOperation, "Cannot modify the query once it has been run or closed."
|
276
|
+
end
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
data/lib/mongo/db.rb
ADDED
@@ -0,0 +1,560 @@
|
|
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
|
+
require 'socket'
|
18
|
+
require 'digest/md5'
|
19
|
+
require 'mutex_m'
|
20
|
+
require 'mongo/collection'
|
21
|
+
require 'mongo/message'
|
22
|
+
require 'mongo/query'
|
23
|
+
require 'mongo/util/ordered_hash.rb'
|
24
|
+
require 'mongo/admin'
|
25
|
+
|
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}'"
|
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
|
143
|
+
|
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
|
162
|
+
close if @socket
|
163
|
+
false
|
164
|
+
end
|
165
|
+
}
|
166
|
+
raise "error: failed to connect to any given host:port" unless @socket
|
167
|
+
end
|
168
|
+
|
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
|
184
|
+
|
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
|
190
|
+
|
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
|
197
|
+
|
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
|
205
|
+
|
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
|
214
|
+
|
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)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
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
|
246
|
+
|
247
|
+
def admin
|
248
|
+
Admin.new(self)
|
249
|
+
end
|
250
|
+
|
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
|
259
|
+
|
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)
|
264
|
+
|
265
|
+
ok?(db_command(:drop => name))
|
266
|
+
end
|
267
|
+
|
268
|
+
# Returns the error message from the most recently executed database
|
269
|
+
# operation for this connection, or +nil+ if there was no error.
|
270
|
+
#
|
271
|
+
# Note: as of this writing, errors are only detected on the db server
|
272
|
+
# for certain kinds of operations (writes). The plan is to change this
|
273
|
+
# so that all operations will set the error if needed.
|
274
|
+
def error
|
275
|
+
doc = db_command(:getlasterror => 1)
|
276
|
+
raise "error retrieving last error: #{doc}" unless ok?(doc)
|
277
|
+
doc['err']
|
278
|
+
end
|
279
|
+
|
280
|
+
# Returns +true+ if an error was caused by the most recently executed
|
281
|
+
# database operation.
|
282
|
+
#
|
283
|
+
# Note: as of this writing, errors are only detected on the db server
|
284
|
+
# for certain kinds of operations (writes). The plan is to change this
|
285
|
+
# so that all operations will set the error if needed.
|
286
|
+
def error?
|
287
|
+
error != nil
|
288
|
+
end
|
289
|
+
|
290
|
+
# Get the most recent error to have occured on this database
|
291
|
+
#
|
292
|
+
# Only returns errors that have occured since the last call to
|
293
|
+
# DB#reset_error_history - returns +nil+ if there is no such error.
|
294
|
+
def previous_error
|
295
|
+
error = db_command(:getpreverror => 1)
|
296
|
+
if error["err"]
|
297
|
+
error
|
298
|
+
else
|
299
|
+
nil
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Reset the error history of this database
|
304
|
+
#
|
305
|
+
# Calls to DB#previous_error will only return errors that have occurred
|
306
|
+
# since the most recent call to this method.
|
307
|
+
def reset_error_history
|
308
|
+
db_command(:reseterror => 1)
|
309
|
+
end
|
310
|
+
|
311
|
+
# Returns true if this database is a master (or is not paired with any
|
312
|
+
# other database), false if it is a slave.
|
313
|
+
def master?
|
314
|
+
doc = db_command(:ismaster => 1)
|
315
|
+
is_master = doc['ismaster']
|
316
|
+
ok?(doc) && is_master.kind_of?(Numeric) && is_master.to_i == 1
|
317
|
+
end
|
318
|
+
|
319
|
+
# Returns a string of the form "host:port" that points to the master
|
320
|
+
# database. Works even if this is the master database.
|
321
|
+
def master
|
322
|
+
doc = db_command(:ismaster => 1)
|
323
|
+
is_master = doc['ismaster']
|
324
|
+
raise "Error retrieving master database: #{doc.inspect}" unless ok?(doc) && is_master.kind_of?(Numeric)
|
325
|
+
case is_master.to_i
|
326
|
+
when 1
|
327
|
+
"#@host:#@port"
|
328
|
+
else
|
329
|
+
doc['remote']
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
# Close the connection to the database.
|
334
|
+
def close
|
335
|
+
if @socket
|
336
|
+
s = @socket
|
337
|
+
@socket = nil
|
338
|
+
s.close
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
def connected?
|
343
|
+
@socket != nil
|
344
|
+
end
|
345
|
+
|
346
|
+
def receive_full(length)
|
347
|
+
message = ""
|
348
|
+
while message.length < length do
|
349
|
+
chunk = @socket.recv(length - message.length)
|
350
|
+
raise "connection closed" unless chunk.length > 0
|
351
|
+
message += chunk
|
352
|
+
end
|
353
|
+
message
|
354
|
+
end
|
355
|
+
|
356
|
+
# Send a MsgMessage to the database.
|
357
|
+
def send_message(msg)
|
358
|
+
send_to_db(MsgMessage.new(msg))
|
359
|
+
end
|
360
|
+
|
361
|
+
# Returns a Cursor over the query results.
|
362
|
+
#
|
363
|
+
# Note that the query gets sent lazily; the cursor calls
|
364
|
+
# #send_query_message when needed. If the caller never requests an
|
365
|
+
# object from the cursor, the query never gets sent.
|
366
|
+
def query(collection, query, admin=false)
|
367
|
+
Cursor.new(self, collection, query, admin)
|
368
|
+
end
|
369
|
+
|
370
|
+
# Used by a Cursor to lazily send the query to the database.
|
371
|
+
def send_query_message(query_message)
|
372
|
+
send_to_db(query_message)
|
373
|
+
end
|
374
|
+
|
375
|
+
# Remove the records that match +selector+ from +collection_name+.
|
376
|
+
# Normally called by Collection#remove or Collection#clear.
|
377
|
+
def remove_from_db(collection_name, selector)
|
378
|
+
_synchronize {
|
379
|
+
send_to_db(RemoveMessage.new(@name, collection_name, selector))
|
380
|
+
}
|
381
|
+
end
|
382
|
+
|
383
|
+
# Update records in +collection_name+ that match +selector+ by
|
384
|
+
# applying +obj+ as an update. Normally called by Collection#replace.
|
385
|
+
def replace_in_db(collection_name, selector, obj)
|
386
|
+
_synchronize {
|
387
|
+
send_to_db(UpdateMessage.new(@name, collection_name, selector, obj, false))
|
388
|
+
}
|
389
|
+
end
|
390
|
+
|
391
|
+
# Update records in +collection_name+ that match +selector+ by
|
392
|
+
# applying +obj+ as an update. If no match, inserts (???). Normally
|
393
|
+
# called by Collection#repsert.
|
394
|
+
def repsert_in_db(collection_name, selector, obj)
|
395
|
+
_synchronize {
|
396
|
+
obj = @pk_factory.create_pk(obj) if @pk_factory
|
397
|
+
send_to_db(UpdateMessage.new(@name, collection_name, selector, obj, true))
|
398
|
+
obj
|
399
|
+
}
|
400
|
+
end
|
401
|
+
|
402
|
+
# Dereference a DBRef, getting the document it points to.
|
403
|
+
def dereference(dbref)
|
404
|
+
collection(dbref.namespace).find_one("_id" => dbref.object_id)
|
405
|
+
end
|
406
|
+
|
407
|
+
# Evaluate a JavaScript expression on MongoDB.
|
408
|
+
# +code+ should be a string or Code instance containing a JavaScript
|
409
|
+
# expression. Additional arguments will be passed to that expression
|
410
|
+
# when it is run on the server.
|
411
|
+
def eval(code, *args)
|
412
|
+
if not code.is_a? Code
|
413
|
+
code = Code.new(code)
|
414
|
+
end
|
415
|
+
|
416
|
+
oh = OrderedHash.new
|
417
|
+
oh[:$eval] = code
|
418
|
+
oh[:args] = args
|
419
|
+
doc = db_command(oh)
|
420
|
+
return doc['retval'] if ok?(doc)
|
421
|
+
raise OperationFailure, "eval failed: #{doc['errmsg']}"
|
422
|
+
end
|
423
|
+
|
424
|
+
# Rename collection +from+ to +to+. Meant to be called by
|
425
|
+
# Collection#rename.
|
426
|
+
def rename_collection(from, to)
|
427
|
+
oh = OrderedHash.new
|
428
|
+
oh[:renameCollection] = "#{@name}.#{from}"
|
429
|
+
oh[:to] = "#{@name}.#{to}"
|
430
|
+
doc = db_command(oh, true)
|
431
|
+
raise "Error renaming collection: #{doc.inspect}" unless ok?(doc)
|
432
|
+
end
|
433
|
+
|
434
|
+
# Drop index +name+ from +collection_name+. Normally called from
|
435
|
+
# Collection#drop_index or Collection#drop_indexes.
|
436
|
+
def drop_index(collection_name, name)
|
437
|
+
oh = OrderedHash.new
|
438
|
+
oh[:deleteIndexes] = collection_name
|
439
|
+
oh[:index] = name
|
440
|
+
doc = db_command(oh)
|
441
|
+
raise "Error with drop_index command: #{doc.inspect}" unless ok?(doc)
|
442
|
+
end
|
443
|
+
|
444
|
+
# Get information on the indexes for the collection +collection_name+.
|
445
|
+
# Normally called by Collection#index_information. Returns a hash where
|
446
|
+
# the keys are index names (as returned by Collection#create_index and
|
447
|
+
# the values are lists of [key, direction] pairs specifying the index
|
448
|
+
# (as passed to Collection#create_index).
|
449
|
+
def index_information(collection_name)
|
450
|
+
sel = {:ns => full_coll_name(collection_name)}
|
451
|
+
info = {}
|
452
|
+
query(Collection.new(self, SYSTEM_INDEX_COLLECTION), Query.new(sel)).each { |index|
|
453
|
+
info[index['name']] = index['key'].to_a
|
454
|
+
}
|
455
|
+
info
|
456
|
+
end
|
457
|
+
|
458
|
+
# Create a new index on +collection_name+. +field_or_spec+
|
459
|
+
# should be either a single field name or a Array of [field name,
|
460
|
+
# direction] pairs. Directions should be specified as
|
461
|
+
# Mongo::ASCENDING or Mongo::DESCENDING. Normally called
|
462
|
+
# by Collection#create_index. If +unique+ is true the index will
|
463
|
+
# enforce a uniqueness constraint.
|
464
|
+
def create_index(collection_name, field_or_spec, unique=false)
|
465
|
+
field_h = OrderedHash.new
|
466
|
+
if field_or_spec.is_a?(String) || field_or_spec.is_a?(Symbol)
|
467
|
+
field_h[field_or_spec.to_s] = 1
|
468
|
+
else
|
469
|
+
field_or_spec.each { |f| field_h[f[0].to_s] = f[1] }
|
470
|
+
end
|
471
|
+
name = gen_index_name(field_h)
|
472
|
+
sel = {
|
473
|
+
:name => name,
|
474
|
+
:ns => full_coll_name(collection_name),
|
475
|
+
:key => field_h,
|
476
|
+
:unique => unique
|
477
|
+
}
|
478
|
+
_synchronize {
|
479
|
+
send_to_db(InsertMessage.new(@name, SYSTEM_INDEX_COLLECTION, false, sel))
|
480
|
+
}
|
481
|
+
name
|
482
|
+
end
|
483
|
+
|
484
|
+
# Insert +objects+ into +collection_name+. Normally called by
|
485
|
+
# Collection#insert. Returns a new array containing the _ids
|
486
|
+
# of the inserted documents.
|
487
|
+
def insert_into_db(collection_name, objects)
|
488
|
+
_synchronize {
|
489
|
+
if @pk_factory
|
490
|
+
objects.collect! { |o|
|
491
|
+
@pk_factory.create_pk(o)
|
492
|
+
}
|
493
|
+
else
|
494
|
+
objects = objects.collect do |o|
|
495
|
+
o[:_id] || o['_id'] ? o : o.merge!(:_id => ObjectID.new)
|
496
|
+
end
|
497
|
+
end
|
498
|
+
send_to_db(InsertMessage.new(@name, collection_name, true, *objects))
|
499
|
+
objects.collect { |o| o[:_id] || o['_id'] }
|
500
|
+
}
|
501
|
+
end
|
502
|
+
|
503
|
+
def send_to_db(message)
|
504
|
+
connect_to_master if !connected? && @auto_reconnect
|
505
|
+
begin
|
506
|
+
@logger.debug(" MONGODB #{message}") if @logger
|
507
|
+
@socket.print(message.buf.to_s)
|
508
|
+
@socket.flush
|
509
|
+
rescue => ex
|
510
|
+
close
|
511
|
+
raise ex
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
def full_coll_name(collection_name)
|
516
|
+
"#{@name}.#{collection_name}"
|
517
|
+
end
|
518
|
+
|
519
|
+
# Return +true+ if +doc+ contains an 'ok' field with the value 1.
|
520
|
+
def ok?(doc)
|
521
|
+
ok = doc['ok']
|
522
|
+
ok.kind_of?(Numeric) && ok.to_i == 1
|
523
|
+
end
|
524
|
+
|
525
|
+
# DB commands need to be ordered, so selector must be an OrderedHash
|
526
|
+
# (or a Hash with only one element). What DB commands really need is
|
527
|
+
# that the "command" key be first.
|
528
|
+
#
|
529
|
+
# Do not call this. Intended for driver use only.
|
530
|
+
def db_command(selector, use_admin_db=false)
|
531
|
+
if !selector.kind_of?(OrderedHash)
|
532
|
+
if !selector.kind_of?(Hash) || selector.keys.length > 1
|
533
|
+
raise "db_command must be given an OrderedHash when there is more than one key"
|
534
|
+
end
|
535
|
+
end
|
536
|
+
|
537
|
+
q = Query.new(selector)
|
538
|
+
q.number_to_return = 1
|
539
|
+
query(Collection.new(self, SYSTEM_COMMAND_COLLECTION), q, use_admin_db).next_object
|
540
|
+
end
|
541
|
+
|
542
|
+
def _synchronize &block
|
543
|
+
@semaphore.synchronize &block
|
544
|
+
end
|
545
|
+
|
546
|
+
private
|
547
|
+
|
548
|
+
def hash_password(username, plaintext)
|
549
|
+
Digest::MD5.hexdigest("#{username}:mongo:#{plaintext}")
|
550
|
+
end
|
551
|
+
|
552
|
+
def gen_index_name(spec)
|
553
|
+
temp = []
|
554
|
+
spec.each_pair { |field, direction|
|
555
|
+
temp = temp.push("#{field}_#{direction}")
|
556
|
+
}
|
557
|
+
return temp.join("_")
|
558
|
+
end
|
559
|
+
end
|
560
|
+
end
|