mongodb-mongo 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README.rdoc +216 -0
- data/Rakefile +54 -0
- data/bin/mongo_console +21 -0
- data/bin/validate +51 -0
- data/examples/benchmarks.rb +38 -0
- data/examples/blog.rb +76 -0
- data/examples/index_test.rb +128 -0
- data/examples/simple.rb +17 -0
- data/lib/mongo/admin.rb +86 -0
- data/lib/mongo/collection.rb +161 -0
- data/lib/mongo/cursor.rb +230 -0
- data/lib/mongo/db.rb +399 -0
- data/lib/mongo/message/get_more_message.rb +21 -0
- data/lib/mongo/message/insert_message.rb +19 -0
- data/lib/mongo/message/kill_cursors_message.rb +20 -0
- data/lib/mongo/message/message.rb +68 -0
- data/lib/mongo/message/message_header.rb +34 -0
- data/lib/mongo/message/msg_message.rb +17 -0
- data/lib/mongo/message/opcodes.rb +16 -0
- data/lib/mongo/message/query_message.rb +67 -0
- data/lib/mongo/message/remove_message.rb +20 -0
- data/lib/mongo/message/update_message.rb +21 -0
- data/lib/mongo/message.rb +4 -0
- data/lib/mongo/mongo.rb +98 -0
- data/lib/mongo/query.rb +110 -0
- data/lib/mongo/types/binary.rb +34 -0
- data/lib/mongo/types/dbref.rb +37 -0
- data/lib/mongo/types/objectid.rb +137 -0
- data/lib/mongo/types/regexp_of_holding.rb +44 -0
- data/lib/mongo/types/undefined.rb +31 -0
- data/lib/mongo/util/bson.rb +431 -0
- data/lib/mongo/util/byte_buffer.rb +163 -0
- data/lib/mongo/util/ordered_hash.rb +68 -0
- data/lib/mongo/util/xml_to_ruby.rb +102 -0
- data/lib/mongo.rb +12 -0
- data/mongo-ruby-driver.gemspec +62 -0
- data/tests/test_admin.rb +60 -0
- data/tests/test_bson.rb +135 -0
- data/tests/test_byte_buffer.rb +69 -0
- data/tests/test_cursor.rb +66 -0
- data/tests/test_db.rb +85 -0
- data/tests/test_db_api.rb +354 -0
- data/tests/test_db_connection.rb +17 -0
- data/tests/test_message.rb +35 -0
- data/tests/test_objectid.rb +98 -0
- data/tests/test_ordered_hash.rb +85 -0
- data/tests/test_round_trip.rb +116 -0
- metadata +100 -0
data/lib/mongo/db.rb
ADDED
@@ -0,0 +1,399 @@
|
|
1
|
+
# --
|
2
|
+
# Copyright (C) 2008-2009 10gen Inc.
|
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.
|
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.
|
12
|
+
#
|
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/>.
|
15
|
+
# ++
|
16
|
+
|
17
|
+
require 'socket'
|
18
|
+
require 'mutex_m'
|
19
|
+
require 'mongo/collection'
|
20
|
+
require 'mongo/message'
|
21
|
+
require 'mongo/query'
|
22
|
+
require 'mongo/util/ordered_hash.rb'
|
23
|
+
require 'mongo/admin'
|
24
|
+
|
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
|
+
# A primary key factory object (or +nil+). See the README.doc file or
|
63
|
+
# DB#new for details.
|
64
|
+
attr_reader :pk_factory
|
65
|
+
|
66
|
+
def pk_factory=(pk_factory)
|
67
|
+
raise "error: can not change PK factory" if @pk_factory
|
68
|
+
@pk_factory = pk_factory
|
69
|
+
end
|
70
|
+
|
71
|
+
# db_name :: The database name
|
72
|
+
#
|
73
|
+
# nodes :: An array of [host, port] pairs.
|
74
|
+
#
|
75
|
+
# options :: A hash of options.
|
76
|
+
#
|
77
|
+
# Options:
|
78
|
+
#
|
79
|
+
# :strict :: If true, collections must exist to be accessed and must
|
80
|
+
# not exist to be created. See #collection and
|
81
|
+
# #create_collection.
|
82
|
+
#
|
83
|
+
# :pk :: A primary key factory object that must respond to :create_pk,
|
84
|
+
# which should take a hash and return a hash which merges the
|
85
|
+
# original hash with any primary key fields the factory wishes
|
86
|
+
# to inject. (NOTE: if the object already has a primary key,
|
87
|
+
# the factory should not inject a new key; this means that the
|
88
|
+
# object is being used in a repsert but it already exists.) The
|
89
|
+
# idea here is that when ever a record is inserted, the :pk
|
90
|
+
# object's +create_pk+ method will be called and the new hash
|
91
|
+
# returned will be inserted.
|
92
|
+
#
|
93
|
+
# When a DB object first connects, it tries the first node. If that
|
94
|
+
# fails, it keeps trying to connect to the remaining nodes until it
|
95
|
+
# sucessfully connects.
|
96
|
+
def initialize(db_name, nodes, options={})
|
97
|
+
raise "Invalid DB name" if !db_name || (db_name && db_name.length > 0 && db_name.include?("."))
|
98
|
+
@name, @nodes = db_name, nodes
|
99
|
+
@strict = options[:strict]
|
100
|
+
@pk_factory = options[:pk]
|
101
|
+
@semaphore = Object.new
|
102
|
+
@semaphore.extend Mutex_m
|
103
|
+
connect_to_first_available_host
|
104
|
+
end
|
105
|
+
|
106
|
+
def connect_to_first_available_host
|
107
|
+
close if @socket
|
108
|
+
@host = @port = nil
|
109
|
+
@nodes.detect { |hp|
|
110
|
+
@host, @port = *hp
|
111
|
+
begin
|
112
|
+
@socket = TCPSocket.new(@host, @port)
|
113
|
+
break if ok?(db_command(:ismaster => 1)) # success
|
114
|
+
rescue => ex
|
115
|
+
close if @socket
|
116
|
+
end
|
117
|
+
@socket
|
118
|
+
}
|
119
|
+
raise "error: failed to connect to any given host:port" unless @socket
|
120
|
+
end
|
121
|
+
|
122
|
+
# Returns an array of collection names. Each name is of the form
|
123
|
+
# "database_name.collection_name".
|
124
|
+
def collection_names
|
125
|
+
names = collections_info.collect { |doc| doc['name'] || '' }
|
126
|
+
names.delete('')
|
127
|
+
names
|
128
|
+
end
|
129
|
+
|
130
|
+
# Returns a cursor over query result hashes. Each hash contains a
|
131
|
+
# 'name' string and optionally an 'options' hash. If +coll_name+ is
|
132
|
+
# specified, an array of length 1 is returned.
|
133
|
+
def collections_info(coll_name=nil)
|
134
|
+
selector = {}
|
135
|
+
selector[:name] = full_coll_name(coll_name) if coll_name
|
136
|
+
query(Collection.new(self, SYSTEM_NAMESPACE_COLLECTION), Query.new(selector))
|
137
|
+
end
|
138
|
+
|
139
|
+
# Create a collection. If +strict+ is false, will return existing or
|
140
|
+
# new collection. If +strict+ is true, will raise an error if
|
141
|
+
# collection +name+ already exists.
|
142
|
+
#
|
143
|
+
# Options is an optional hash:
|
144
|
+
#
|
145
|
+
# :capped :: Boolean. If not specified, capped is +false+.
|
146
|
+
#
|
147
|
+
# :size :: If +capped+ is +true+, specifies the maximum number of
|
148
|
+
# bytes. If +false+, specifies the initial extent of the
|
149
|
+
# collection.
|
150
|
+
#
|
151
|
+
# :max :: Max number of records in a capped collection. Optional.
|
152
|
+
def create_collection(name, options={})
|
153
|
+
# First check existence
|
154
|
+
if collection_names.include?(full_coll_name(name))
|
155
|
+
if strict?
|
156
|
+
raise "Collection #{name} already exists. Currently in strict mode."
|
157
|
+
else
|
158
|
+
return Collection.new(self, name)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# Create new collection
|
163
|
+
oh = OrderedHash.new
|
164
|
+
oh[:create] = name
|
165
|
+
doc = db_command(oh.merge(options || {}))
|
166
|
+
ok = doc['ok']
|
167
|
+
return Collection.new(self, name) if ok.kind_of?(Numeric) && (ok.to_i == 1 || ok.to_i == 0)
|
168
|
+
raise "Error creating collection: #{doc.inspect}"
|
169
|
+
end
|
170
|
+
|
171
|
+
def admin
|
172
|
+
Admin.new(self)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Return a collection. If +strict+ is false, will return existing or
|
176
|
+
# new collection. If +strict+ is true, will raise an error if
|
177
|
+
# collection +name+ does not already exists.
|
178
|
+
def collection(name)
|
179
|
+
return Collection.new(self, name) if !strict? || collection_names.include?(full_coll_name(name))
|
180
|
+
raise "Collection #{name} doesn't exist. Currently in strict mode."
|
181
|
+
end
|
182
|
+
|
183
|
+
# Drop collection +name+. Returns +true+ on success or if the
|
184
|
+
# collection does not exist, +false+ otherwise.
|
185
|
+
def drop_collection(name)
|
186
|
+
return true unless collection_names.include?(full_coll_name(name))
|
187
|
+
|
188
|
+
coll = collection(name)
|
189
|
+
coll.drop_indexes # Mongo requires that we drop indexes manually
|
190
|
+
ok?(db_command(:drop => name))
|
191
|
+
end
|
192
|
+
|
193
|
+
# Returns true if this database is a master (or is not paired with any
|
194
|
+
# other database), false if it is a slave.
|
195
|
+
def master?
|
196
|
+
doc = db_command(:ismaster => 1)
|
197
|
+
is_master = doc['ismaster']
|
198
|
+
ok?(doc) && is_master.kind_of?(Numeric) && is_master.to_i == 1
|
199
|
+
end
|
200
|
+
|
201
|
+
# Returns a string of the form "host:port" that points to the master
|
202
|
+
# database. Works even if this is the master database.
|
203
|
+
def master
|
204
|
+
doc = db_command(:ismaster => 1)
|
205
|
+
is_master = doc['ismaster']
|
206
|
+
raise "Error retrieving master database" unless ok?(doc) && is_master.kind_of?(Numeric)
|
207
|
+
case is_master.to_i
|
208
|
+
when 1
|
209
|
+
"#@host:#@port"
|
210
|
+
else
|
211
|
+
doc['remote']
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Switches our socket to the master database. If we are already the
|
216
|
+
# master, no change is made.
|
217
|
+
def switch_to_master
|
218
|
+
master_str = master()
|
219
|
+
unless master_str == "#@host:#@port"
|
220
|
+
@semaphore.synchronize {
|
221
|
+
master_str =~ /(.+):(\d+)/
|
222
|
+
@host, @port = $1, $2
|
223
|
+
close()
|
224
|
+
@socket = TCPSocket.new(@host, @port)
|
225
|
+
}
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
# Close the connection to the database.
|
230
|
+
def close
|
231
|
+
@socket.close if @socket
|
232
|
+
@socket = nil
|
233
|
+
end
|
234
|
+
|
235
|
+
def connected?
|
236
|
+
@socket != nil
|
237
|
+
end
|
238
|
+
|
239
|
+
# Send a MsgMessage to the database.
|
240
|
+
def send_message(msg)
|
241
|
+
send_to_db(MsgMessage.new(msg))
|
242
|
+
end
|
243
|
+
|
244
|
+
# Returns a Cursor over the query results.
|
245
|
+
#
|
246
|
+
# Note that the query gets sent lazily; the cursor calls
|
247
|
+
# #send_query_message when needed. If the caller never requests an
|
248
|
+
# object from the cursor, the query never gets sent.
|
249
|
+
def query(collection, query)
|
250
|
+
Cursor.new(self, collection, query)
|
251
|
+
end
|
252
|
+
|
253
|
+
# Used by a Cursor to lazily send the query to the database.
|
254
|
+
def send_query_message(query_message)
|
255
|
+
@semaphore.synchronize {
|
256
|
+
send_to_db(query_message)
|
257
|
+
}
|
258
|
+
end
|
259
|
+
|
260
|
+
# Remove the records that match +selector+ from +collection_name+.
|
261
|
+
# Normally called by Collection#remove or Collection#clear.
|
262
|
+
def remove_from_db(collection_name, selector)
|
263
|
+
@semaphore.synchronize {
|
264
|
+
send_to_db(RemoveMessage.new(@name, collection_name, selector))
|
265
|
+
}
|
266
|
+
end
|
267
|
+
|
268
|
+
# Update records in +collection_name+ that match +selector+ by
|
269
|
+
# applying +obj+ as an update. Normally called by Collection#replace.
|
270
|
+
def replace_in_db(collection_name, selector, obj)
|
271
|
+
@semaphore.synchronize {
|
272
|
+
send_to_db(UpdateMessage.new(@name, collection_name, selector, obj, false))
|
273
|
+
}
|
274
|
+
end
|
275
|
+
|
276
|
+
# Alias for #replace_in_db. Normally called by Collection.modify.
|
277
|
+
alias_method :modify_in_db, :replace_in_db
|
278
|
+
|
279
|
+
# Update records in +collection_name+ that match +selector+ by
|
280
|
+
# applying +obj+ as an update. If no match, inserts (???). Normally
|
281
|
+
# called by Collection#repsert.
|
282
|
+
def repsert_in_db(collection_name, selector, obj)
|
283
|
+
@semaphore.synchronize {
|
284
|
+
obj = @pk_factory.create_pk(obj) if @pk_factory
|
285
|
+
send_to_db(UpdateMessage.new(@name, collection_name, selector, obj, true))
|
286
|
+
obj
|
287
|
+
}
|
288
|
+
end
|
289
|
+
|
290
|
+
# Return the number of records in +collection_name+ that match
|
291
|
+
# +selector+. If +selector+ is +nil+ or an empty hash, returns the
|
292
|
+
# count of all records. Normally called by Collection#count.
|
293
|
+
def count(collection_name, selector={})
|
294
|
+
oh = OrderedHash.new
|
295
|
+
oh[:count] = collection_name
|
296
|
+
oh[:query] = selector || {}
|
297
|
+
doc = db_command(oh)
|
298
|
+
return doc['n'].to_i if ok?(doc)
|
299
|
+
raise "Error with count command: #{doc.inspect}"
|
300
|
+
end
|
301
|
+
|
302
|
+
# Drop index +name+ from +collection_name+. Normally called from
|
303
|
+
# Collection#drop_index or Collection#drop_indexes.
|
304
|
+
def drop_index(collection_name, name)
|
305
|
+
oh = OrderedHash.new
|
306
|
+
oh[:deleteIndexes] = collection_name
|
307
|
+
oh[:index] = name
|
308
|
+
doc = db_command(oh)
|
309
|
+
raise "Error with drop_index command: #{doc.inspect}" unless ok?(doc)
|
310
|
+
end
|
311
|
+
|
312
|
+
# Return an array of hashes, one for each index on +collection_name+.
|
313
|
+
# Normally called by Collection#index_information. Each hash contains:
|
314
|
+
#
|
315
|
+
# :name :: Index name
|
316
|
+
#
|
317
|
+
# :keys :: Hash whose keys are the names of the fields that make up
|
318
|
+
# the key and values are integers.
|
319
|
+
#
|
320
|
+
# :ns :: Namespace; same as +collection_name+.
|
321
|
+
def index_information(collection_name)
|
322
|
+
sel = {:ns => full_coll_name(collection_name)}
|
323
|
+
query(Collection.new(self, SYSTEM_INDEX_COLLECTION), Query.new(sel)).collect { |row|
|
324
|
+
h = {:name => row['name']}
|
325
|
+
raise "Name of index on return from db was nil. Coll = #{full_coll_name(collection_name)}" unless h[:name]
|
326
|
+
|
327
|
+
h[:keys] = row['key']
|
328
|
+
raise "Keys for index on return from db was nil. Coll = #{full_coll_name(collection_name)}" unless h[:keys]
|
329
|
+
|
330
|
+
h[:ns] = row['ns']
|
331
|
+
raise "Namespace for index on return from db was nil. Coll = #{full_coll_name(collection_name)}" unless h[:ns]
|
332
|
+
h[:ns].sub!(/.*\./, '')
|
333
|
+
raise "Error: ns != collection" unless h[:ns] == collection_name
|
334
|
+
|
335
|
+
h
|
336
|
+
}
|
337
|
+
end
|
338
|
+
|
339
|
+
# Create a new index on +collection_name+ named +index_name+. +fields+
|
340
|
+
# should be an array of field names. Normally called by
|
341
|
+
# Collection#create_index.
|
342
|
+
def create_index(collection_name, index_name, fields)
|
343
|
+
sel = {:name => index_name, :ns => full_coll_name(collection_name)}
|
344
|
+
field_h = {}
|
345
|
+
fields.each { |f| field_h[f] = 1 }
|
346
|
+
sel[:key] = field_h
|
347
|
+
@semaphore.synchronize {
|
348
|
+
send_to_db(InsertMessage.new(@name, SYSTEM_INDEX_COLLECTION, sel))
|
349
|
+
}
|
350
|
+
end
|
351
|
+
|
352
|
+
# Insert +objects+ into +collection_name+. Normally called by
|
353
|
+
# Collection#insert. Returns a new array containing +objects+,
|
354
|
+
# possibly modified by @pk_factory.
|
355
|
+
def insert_into_db(collection_name, objects)
|
356
|
+
@semaphore.synchronize {
|
357
|
+
objects.collect { |o|
|
358
|
+
o = @pk_factory.create_pk(o) if @pk_factory
|
359
|
+
send_to_db(InsertMessage.new(@name, collection_name, o))
|
360
|
+
o
|
361
|
+
}
|
362
|
+
}
|
363
|
+
end
|
364
|
+
|
365
|
+
def send_to_db(message)
|
366
|
+
@socket.print(message.buf.to_s)
|
367
|
+
end
|
368
|
+
|
369
|
+
def full_coll_name(collection_name)
|
370
|
+
"#{@name}.#{collection_name}"
|
371
|
+
end
|
372
|
+
|
373
|
+
# Return +true+ if +doc+ contains an 'ok' field with the value 1.
|
374
|
+
def ok?(doc)
|
375
|
+
ok = doc['ok']
|
376
|
+
ok.kind_of?(Numeric) && ok.to_i == 1
|
377
|
+
end
|
378
|
+
|
379
|
+
# DB commands need to be ordered, so selector must be an OrderedHash
|
380
|
+
# (or a Hash with only one element). What DB commands really need is
|
381
|
+
# that the "command" key be first.
|
382
|
+
#
|
383
|
+
# Do not call this. Intended for driver use only.
|
384
|
+
def db_command(selector)
|
385
|
+
if !selector.kind_of?(OrderedHash)
|
386
|
+
if !selector.kind_of?(Hash) || selector.keys.length > 1
|
387
|
+
raise "db_command must be given an OrderedHash when there is more than one key"
|
388
|
+
end
|
389
|
+
end
|
390
|
+
|
391
|
+
q = Query.new(selector)
|
392
|
+
q.number_to_return = 1
|
393
|
+
query(Collection.new(self, SYSTEM_COMMAND_COLLECTION), q).next_object
|
394
|
+
end
|
395
|
+
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'mongo/message/message'
|
2
|
+
require 'mongo/message/opcodes'
|
3
|
+
|
4
|
+
module XGen
|
5
|
+
module Mongo
|
6
|
+
module Driver
|
7
|
+
|
8
|
+
class GetMoreMessage < Message
|
9
|
+
|
10
|
+
def initialize(db_name, collection_name, cursor)
|
11
|
+
super(OP_GET_MORE)
|
12
|
+
write_int(0)
|
13
|
+
write_string("#{db_name}.#{collection_name}")
|
14
|
+
write_int(0) # num to return; leave it up to the db for now
|
15
|
+
write_long(cursor)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'mongo/message/message'
|
2
|
+
require 'mongo/message/opcodes'
|
3
|
+
|
4
|
+
module XGen
|
5
|
+
module Mongo
|
6
|
+
module Driver
|
7
|
+
|
8
|
+
class InsertMessage < Message
|
9
|
+
|
10
|
+
def initialize(db_name, collection_name, *objs)
|
11
|
+
super(OP_INSERT)
|
12
|
+
write_int(0)
|
13
|
+
write_string("#{db_name}.#{collection_name}")
|
14
|
+
objs.each { |o| write_doc(o) }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'mongo/message/message'
|
2
|
+
require 'mongo/message/opcodes'
|
3
|
+
|
4
|
+
module XGen
|
5
|
+
module Mongo
|
6
|
+
module Driver
|
7
|
+
|
8
|
+
class KillCursorsMessage < Message
|
9
|
+
|
10
|
+
def initialize(*cursors)
|
11
|
+
super(OP_KILL_CURSORS)
|
12
|
+
write_int(0)
|
13
|
+
write_int(cursors.length)
|
14
|
+
cursors.each { |c| write_long c }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'mongo/util/bson'
|
2
|
+
require 'mongo/util/byte_buffer'
|
3
|
+
|
4
|
+
module XGen
|
5
|
+
module Mongo
|
6
|
+
module Driver
|
7
|
+
|
8
|
+
class Message
|
9
|
+
|
10
|
+
HEADER_SIZE = 16 # size, id, response_to, opcode
|
11
|
+
|
12
|
+
@@class_req_id = 0
|
13
|
+
|
14
|
+
attr_reader :buf # for testing
|
15
|
+
|
16
|
+
def initialize(op)
|
17
|
+
@op = op
|
18
|
+
@message_length = HEADER_SIZE
|
19
|
+
@data_length = 0
|
20
|
+
@request_id = (@@class_req_id += 1)
|
21
|
+
@response_id = 0
|
22
|
+
@buf = ByteBuffer.new
|
23
|
+
|
24
|
+
@buf.put_int(16) # holder for length
|
25
|
+
@buf.put_int(@request_id)
|
26
|
+
@buf.put_int(0) # response_to
|
27
|
+
@buf.put_int(op)
|
28
|
+
end
|
29
|
+
|
30
|
+
def write_int(i)
|
31
|
+
@buf.put_int(i)
|
32
|
+
update_message_length
|
33
|
+
end
|
34
|
+
|
35
|
+
def write_long(i)
|
36
|
+
@buf.put_long(i)
|
37
|
+
update_message_length
|
38
|
+
end
|
39
|
+
|
40
|
+
def write_string(s)
|
41
|
+
BSON.serialize_cstr(@buf, s)
|
42
|
+
update_message_length
|
43
|
+
end
|
44
|
+
|
45
|
+
def write_doc(hash)
|
46
|
+
@buf.put_array(BSON.new.serialize(hash).to_a)
|
47
|
+
update_message_length
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_a
|
51
|
+
@buf.to_a
|
52
|
+
end
|
53
|
+
|
54
|
+
def dump
|
55
|
+
@buf.dump
|
56
|
+
end
|
57
|
+
|
58
|
+
# Do not call. Private, but kept public for testing.
|
59
|
+
def update_message_length
|
60
|
+
pos = @buf.position
|
61
|
+
@buf.put_int(@buf.size, 0)
|
62
|
+
@buf.position = pos
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'mongo/util/byte_buffer'
|
2
|
+
|
3
|
+
module XGen
|
4
|
+
module Mongo
|
5
|
+
module Driver
|
6
|
+
|
7
|
+
class MessageHeader
|
8
|
+
|
9
|
+
HEADER_SIZE = 16
|
10
|
+
|
11
|
+
def initialize()
|
12
|
+
@buf = ByteBuffer.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def read_header(socket)
|
16
|
+
@buf.rewind
|
17
|
+
@buf.put_array(socket.recv(HEADER_SIZE).unpack("C*"))
|
18
|
+
raise "Short read for DB response header: expected #{HEADER_SIZE} bytes, saw #{@buf.size}" unless @buf.size == HEADER_SIZE
|
19
|
+
@buf.rewind
|
20
|
+
@size = @buf.get_int
|
21
|
+
@request_id = @buf.get_int
|
22
|
+
@response_to = @buf.get_int
|
23
|
+
@op = @buf.get_int
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def dump
|
28
|
+
@buf.dump
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module XGen
|
2
|
+
module Mongo
|
3
|
+
module Driver
|
4
|
+
OP_REPLY = 1 # reply. responseTo is set.
|
5
|
+
OP_MSG = 1000 # generic msg command followed by a string
|
6
|
+
OP_UPDATE = 2001 # update object
|
7
|
+
OP_INSERT = 2002
|
8
|
+
# GET_BY_OID = 2003
|
9
|
+
OP_QUERY = 2004
|
10
|
+
OP_GET_MORE = 2005
|
11
|
+
OP_DELETE = 2006
|
12
|
+
OP_KILL_CURSORS = 2007
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'mongo/message/message'
|
2
|
+
require 'mongo/message/opcodes'
|
3
|
+
require 'mongo/util/ordered_hash'
|
4
|
+
|
5
|
+
module XGen
|
6
|
+
module Mongo
|
7
|
+
module Driver
|
8
|
+
|
9
|
+
class QueryMessage < Message
|
10
|
+
|
11
|
+
attr_reader :query
|
12
|
+
|
13
|
+
def initialize(db_name, collection_name, query)
|
14
|
+
super(OP_QUERY)
|
15
|
+
@query = query
|
16
|
+
write_int(0)
|
17
|
+
write_string("#{db_name}.#{collection_name}")
|
18
|
+
write_int(query.number_to_skip)
|
19
|
+
write_int(query.number_to_return)
|
20
|
+
sel = query.selector
|
21
|
+
if query.contains_special_fields
|
22
|
+
sel = OrderedHash.new
|
23
|
+
sel['query'] = query.selector
|
24
|
+
if query.order_by && query.order_by.length > 0
|
25
|
+
sel['orderby'] = case query.order_by
|
26
|
+
when String
|
27
|
+
{query.order_by => 1}
|
28
|
+
when Array
|
29
|
+
h = OrderedHash.new
|
30
|
+
query.order_by.each { |ob|
|
31
|
+
case ob
|
32
|
+
when String
|
33
|
+
h[ob] = 1
|
34
|
+
when Hash # should have one entry; will handle all
|
35
|
+
ob.each { |k,v| h[k] = v }
|
36
|
+
else
|
37
|
+
raise "illegal query order_by value #{query.order_by.inspect}"
|
38
|
+
end
|
39
|
+
}
|
40
|
+
h
|
41
|
+
when Hash # Should be an ordered hash, but this message doesn't care
|
42
|
+
query.order_by
|
43
|
+
else
|
44
|
+
raise "illegal order_by: is a #{query.order_by.class.name}, must be String, Array, Hash, or OrderedHash"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
if query.hint_fields && query.hint_fields.length > 0
|
48
|
+
hints = OrderedHash.new
|
49
|
+
query.hint_fields.each { |hf| hints[hf] = 1 }
|
50
|
+
sel['$hint'] = hints
|
51
|
+
end
|
52
|
+
if query.explain
|
53
|
+
sel['$explain'] = true
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
57
|
+
write_doc(sel)
|
58
|
+
write_doc(query.fields) if query.fields
|
59
|
+
end
|
60
|
+
|
61
|
+
def first_key(key)
|
62
|
+
@first_key = key
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'mongo/message/message'
|
2
|
+
require 'mongo/message/opcodes'
|
3
|
+
|
4
|
+
module XGen
|
5
|
+
module Mongo
|
6
|
+
module Driver
|
7
|
+
|
8
|
+
class RemoveMessage < Message
|
9
|
+
|
10
|
+
def initialize(db_name, collection_name, sel)
|
11
|
+
super(OP_DELETE)
|
12
|
+
write_int(0)
|
13
|
+
write_string("#{db_name}.#{collection_name}")
|
14
|
+
write_int(0) # flags?
|
15
|
+
write_doc(sel)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|