mongo 0.0.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.
@@ -0,0 +1,180 @@
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 'mongo/message'
18
+ require 'mongo/util/byte_buffer'
19
+ require 'mongo/util/bson'
20
+
21
+ module XGen
22
+ module Mongo
23
+ module Driver
24
+
25
+ # A cursor over query results. Returned objects are hashes.
26
+ class Cursor
27
+
28
+ include Enumerable
29
+
30
+ RESPONSE_HEADER_SIZE = 20
31
+
32
+ def initialize(db, collection, num_to_return=0)
33
+ @db, @collection, @num_to_return = db, collection, num_to_return
34
+ @cache = []
35
+ @closed = false
36
+ @can_call_to_a = true
37
+ read_all
38
+ end
39
+
40
+ # Return +true+ if there are more records to retrieve. We do not check
41
+ # @num_to_return; #each is responsible for doing that.
42
+ def more?
43
+ num_remaining > 0
44
+ end
45
+
46
+ # Return the next object. Raises an error if necessary.
47
+ def next_object
48
+ refill_via_get_more if num_remaining == 0
49
+ o = @cache.shift
50
+ raise o['$err'] if o['$err']
51
+ o
52
+ end
53
+
54
+ # Iterate over each object, yielding it to the given block. At most
55
+ # @num_to_return records are returned (or all of them, if
56
+ # @num_to_return is 0).
57
+ #
58
+ # If #to_a has already been called then this method uses the array
59
+ # that we store internally. In that case, #each can be called multiple
60
+ # times because it re-uses that array.
61
+ #
62
+ # You can call #each after calling #to_a (multiple times even, because
63
+ # it will use the internally-stored array), but you can't call #to_a
64
+ # after calling #each unless you also called it before calling #each.
65
+ # If you try to do that, an error will be raised.
66
+ def each
67
+ if @rows # Already turned into an array
68
+ @rows.each { |row| yield row }
69
+ else
70
+ num_returned = 0
71
+ while more? && (@num_to_return <= 0 || num_returned < @num_to_return)
72
+ yield next_object()
73
+ num_returned += 1
74
+ end
75
+ @can_call_to_a = false
76
+ end
77
+ end
78
+
79
+ # Return all of the rows (up to the +num_to_return+ value specified in
80
+ # #new) as an array. Calling this multiple times will work fine; it
81
+ # always returns the same array.
82
+ #
83
+ # Don't use this if you're expecting large amounts of data, of course.
84
+ # All of the returned rows are kept in an array stored in this object
85
+ # so it can be reused.
86
+ #
87
+ # You can call #each after calling #to_a (multiple times even, because
88
+ # it will use the internally-stored array), but you can't call #to_a
89
+ # after calling #each unless you also called it before calling #each.
90
+ # If you try to do that, an error will be raised.
91
+ def to_a
92
+ return @rows if @rows
93
+ raise "can't call Cursor#to_a after calling Cursor#each" unless @can_call_to_a
94
+ @rows = []
95
+ num_returned = 0
96
+ while more? && (@num_to_return <= 0 || num_returned < @num_to_return)
97
+ @rows << next_object()
98
+ num_returned += 1
99
+ end
100
+ @rows
101
+ end
102
+
103
+ # Close the cursor.
104
+ def close
105
+ @db.send_to_db(KillCursorMessage(@cursor_id)) if @cursor_id
106
+ @cache = []
107
+ @cursor_id = 0
108
+ @closed = true
109
+ end
110
+
111
+ protected
112
+
113
+ def read_all
114
+ read_message_header
115
+ read_response_header
116
+ read_objects_off_wire
117
+ end
118
+
119
+ def read_objects_off_wire
120
+ while doc = next_object_on_wire
121
+ @cache << doc
122
+ end
123
+ end
124
+
125
+ def read_message_header
126
+ MessageHeader.new.read_header(@db.socket)
127
+ end
128
+
129
+ def read_response_header
130
+ header_buf = ByteBuffer.new
131
+ header_buf.put_array(@db.socket.recv(RESPONSE_HEADER_SIZE).unpack("C*"))
132
+ raise "Short read for DB response header; expected #{RESPONSE_HEADER_SIZE} bytes, saw #{header_buf.length}" unless header_buf.length == RESPONSE_HEADER_SIZE
133
+ header_buf.rewind
134
+ @result_flags = header_buf.get_int
135
+ @cursor_id = header_buf.get_long
136
+ @starting_from = header_buf.get_int
137
+ @n_returned = header_buf.get_int
138
+ @n_remaining = @n_returned
139
+ end
140
+
141
+ def num_remaining
142
+ refill_via_get_more if @cache.length == 0
143
+ @cache.length
144
+ end
145
+
146
+ private
147
+
148
+ def next_object_on_wire
149
+ # if @n_remaining is 0 but we have a non-zero cursor, there are more
150
+ # to fetch, so do a GetMore operation, but don't do it here - do it
151
+ # when someone pulls an object out of the cache and it's empty
152
+ return nil if @n_remaining == 0
153
+ object_from_stream
154
+ end
155
+
156
+ def refill_via_get_more
157
+ return if @cursor_id == 0
158
+ @db.send_to_db(GetMoreMessage.new(@db.name, @collection, @cursor_id))
159
+ read_all
160
+ end
161
+
162
+ def object_from_stream
163
+ buf = ByteBuffer.new
164
+ buf.put_array(@db.socket.recv(4).unpack("C*"))
165
+ buf.rewind
166
+ size = buf.get_int
167
+ buf.put_array(@db.socket.recv(size-4).unpack("C*"), 4)
168
+ @n_remaining -= 1
169
+ buf.rewind
170
+ BSON.new.deserialize(buf)
171
+ end
172
+
173
+ def to_s
174
+ "DBResponse(flags=#@result_flags, cursor_id=#@cursor_id, start=#@starting_from, n_returned=#@n_returned)"
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+
@@ -0,0 +1,307 @@
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/mongo'
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 XGen
27
+ module Mongo
28
+ module Driver
29
+
30
+ # A Mongo database.
31
+ class DB
32
+
33
+ SYSTEM_NAMESPACE_COLLECTION = "system.namespaces"
34
+ SYSTEM_INDEX_COLLECTION = "system.indexes"
35
+ SYSTEM_PROFILE_COLLECTION = "system.profile"
36
+ SYSTEM_COMMAND_COLLECTION = "$cmd"
37
+
38
+ # Strict mode enforces collection existence checks. When +true+,
39
+ # asking for a collection that does not exist or trying to create a
40
+ # collection that already exists raises an error.
41
+ #
42
+ # Strict mode is off (+false+) by default. Its value can be changed at
43
+ # any time.
44
+ attr_writer :strict
45
+
46
+ # Returns the value of the +strict+ flag.
47
+ def strict?; @strict; end
48
+
49
+ # The name of the database.
50
+ attr_reader :name
51
+
52
+ # The database's socket. For internal use only.
53
+ attr_reader :socket
54
+
55
+ # db_name :: The database name
56
+ #
57
+ # host :: The database host name or IP address. Defaults to 'localhost'.
58
+ #
59
+ # port :: The database port number. Defaults to
60
+ # XGen::Mongo::Driver::Mongo::DEFAULT_PORT.
61
+ #
62
+ def initialize(db_name, host='localhost', port=XGen::Mongo::Driver::Mongo::DEFAULT_PORT)
63
+ raise "Invalid DB name" if !db_name || (db_name && db_name.length > 0 && db_name.include?("."))
64
+ @name, @host, @port = db_name, host, port
65
+ @socket = TCPSocket.new(@host, @port)
66
+ @strict = false
67
+ @semaphore = Object.new
68
+ @semaphore.extend Mutex_m
69
+ end
70
+
71
+ # Returns an array of collection names. Each name is of the form
72
+ # "database_name.collection_name".
73
+ def collection_names
74
+ names = collections_info.collect { |doc| doc['name'] || '' }
75
+ names.delete('')
76
+ names
77
+ end
78
+
79
+ # Returns a cursor over query result hashes. Each hash contains a
80
+ # 'name' string and optionally an 'options' hash. If +coll_name+ is
81
+ # specified, an array of length 1 is returned.
82
+ def collections_info(coll_name=nil)
83
+ selector = {}
84
+ selector[:name] = full_coll_name(coll_name) if coll_name
85
+ query(SYSTEM_NAMESPACE_COLLECTION, Query.new(selector))
86
+ end
87
+
88
+ # Create a collection. If +strict+ is false, will return existing or
89
+ # new collection. If +strict+ is true, will raise an error if
90
+ # collection +name+ already exists.
91
+ #
92
+ # Options is an optional hash:
93
+ #
94
+ # :capped :: Boolean. If not specified, capped is +false+.
95
+ #
96
+ # :size :: If +capped+ is +true+, specifies the maximum number of
97
+ # bytes. If +false+, specifies the initial extent of the
98
+ # collection.
99
+ #
100
+ # :max :: Max number of records in a capped collection. Optional.
101
+ def create_collection(name, options={})
102
+ # First check existence
103
+ if collection_names.include?(full_coll_name(name))
104
+ if strict?
105
+ raise "Collection #{name} already exists. Currently in strict mode."
106
+ else
107
+ return Collection.new(self, name)
108
+ end
109
+ end
110
+
111
+ # Create new collection
112
+ oh = OrderedHash.new
113
+ oh[:create] = name
114
+ doc = db_command(oh.merge(options || {}))
115
+ ok = doc['ok']
116
+ return Collection.new(self, name) if ok.kind_of?(Numeric) && (ok.to_i == 1 || ok.to_i == 0)
117
+ raise "Error creating collection: #{doc.inspect}"
118
+ end
119
+
120
+ def admin
121
+ Admin.new(self)
122
+ end
123
+
124
+ # Return a collection. If +strict+ is false, will return existing or
125
+ # new collection. If +strict+ is true, will raise an error if
126
+ # collection +name+ does not already exists.
127
+ def collection(name)
128
+ return Collection.new(self, name) if collection_names.include?(full_coll_name(name))
129
+ if strict?
130
+ raise "Collection #{name} doesn't exist. Currently in strict mode."
131
+ else
132
+ create_collection(name)
133
+ end
134
+ end
135
+
136
+ # Drop collection +name+. Returns +true+ on success or if the
137
+ # collection does not exist, +false+ otherwise.
138
+ def drop_collection(name)
139
+ return true unless collection_names.include?(full_coll_name(name))
140
+
141
+ coll = collection(name)
142
+ coll.drop_indexes # Mongo requires that we drop indexes manually
143
+ ok?(db_command(:drop => name))
144
+ end
145
+
146
+ # Returns true if this database is a master (or is not paired with any
147
+ # other database), false if it is a slave.
148
+ def master?
149
+ doc = db_command(:ismaster => 1)
150
+ is_master = doc['ismaster']
151
+ ok?(doc) && is_master.kind_of?(Numeric) && is_master.to_i == 1
152
+ end
153
+
154
+ # Close the connection to the database.
155
+ def close
156
+ @socket.close
157
+ end
158
+
159
+ # Send a MsgMessage to the database.
160
+ def send_message(msg)
161
+ send_to_db(MsgMessage.new(msg))
162
+ end
163
+
164
+ # Send a Query to +collection_name+ and return a Cursor over the
165
+ # results.
166
+ def query(collection_name, query)
167
+ @semaphore.synchronize {
168
+ send_to_db(QueryMessage.new(@name, collection_name, query))
169
+ Cursor.new(self, collection_name, query.number_to_return)
170
+ }
171
+ end
172
+
173
+ # Remove the records that match +selector+ from +collection_name+.
174
+ # Normally called by Collection#remove or Collection#clear.
175
+ def remove_from_db(collection_name, selector)
176
+ @semaphore.synchronize {
177
+ send_to_db(RemoveMessage.new(@name, collection_name, selector))
178
+ }
179
+ end
180
+
181
+ # Update records in +collection_name+ that match +selector+ by
182
+ # applying +obj+ as an update. Normally called by Collection#replace.
183
+ def replace_in_db(collection_name, selector, obj)
184
+ @semaphore.synchronize {
185
+ send_to_db(UpdateMessage.new(@name, collection_name, selector, obj, false))
186
+ }
187
+ end
188
+
189
+ # Alias for #replace_in_db. Normally called by Collection.modify.
190
+ alias_method :modify_in_db, :replace_in_db
191
+
192
+ # Update records in +collection_name+ that match +selector+ by
193
+ # applying +obj+ as an update. If no match, inserts (???). Normally
194
+ # called by Collection#repsert.
195
+ def repsert_in_db(collection_name, selector, obj)
196
+ # TODO if PKInjector, inject
197
+ @semaphore.synchronize {
198
+ send_to_db(UpdateMessage.new(@name, collection_name, selector, obj, true))
199
+ obj
200
+ }
201
+ end
202
+
203
+ # Return the number of records in +collection_name+ that match
204
+ # +selector+. If +selector+ is +nil+ or an empty hash, returns the
205
+ # count of all records. Normally called by Collection#count.
206
+ def count(collection_name, selector={})
207
+ oh = OrderedHash.new
208
+ oh[:count] = collection_name
209
+ oh[:query] = selector || {}
210
+ doc = db_command(oh)
211
+ return doc['n'].to_i if ok?(doc)
212
+ raise "Error with count command: #{doc.inspect}"
213
+ end
214
+
215
+ # Drop index +name+ from +collection_name+. Normally called from
216
+ # Collection#drop_index or Collection#drop_indexes.
217
+ def drop_index(collection_name, name)
218
+ oh = OrderedHash.new
219
+ oh[:deleteIndexes] = collection_name
220
+ oh[:index] = name
221
+ doc = db_command(oh)
222
+ raise "Error with drop_index command: #{doc.inspect}" unless ok?(doc)
223
+ end
224
+
225
+ # Return an array of hashes, one for each index on +collection_name+.
226
+ # Normally called by Collection#index_information. Each hash contains:
227
+ #
228
+ # :name :: Index name
229
+ #
230
+ # :keys :: Hash whose keys are the names of the fields that make up
231
+ # the key and values are integers.
232
+ #
233
+ # :ns :: Namespace; same as +collection_name+.
234
+ def index_information(collection_name)
235
+ sel = {:ns => full_coll_name(collection_name)}
236
+ query(SYSTEM_INDEX_COLLECTION, Query.new(sel)).collect { |row|
237
+ h = {:name => row['name']}
238
+ raise "Name of index on return from db was nil. Coll = #{full_coll_name(collection_name)}" unless h[:name]
239
+
240
+ h[:keys] = row['key']
241
+ raise "Keys for index on return from db was nil. Coll = #{full_coll_name(collection_name)}" unless h[:keys]
242
+
243
+ h[:ns] = row['ns']
244
+ raise "Namespace for index on return from db was nil. Coll = #{full_coll_name(collection_name)}" unless h[:ns]
245
+ h[:ns].sub!(/.*\./, '')
246
+ raise "Error: ns != collection" unless h[:ns] == collection_name
247
+
248
+ h
249
+ }
250
+ end
251
+
252
+ # Create a new index on +collection_name+ named +index_name+. +fields+
253
+ # should be an array of field names. Normally called by
254
+ # Collection#create_index.
255
+ def create_index(collection_name, index_name, fields)
256
+ sel = {:name => index_name, :ns => full_coll_name(collection_name)}
257
+ field_h = {}
258
+ fields.each { |f| field_h[f] = 1 }
259
+ sel[:key] = field_h
260
+ @semaphore.synchronize {
261
+ send_to_db(InsertMessage.new(@name, SYSTEM_INDEX_COLLECTION, sel))
262
+ }
263
+ end
264
+
265
+ # Insert +objects+ into +collection_name+. Normally called by
266
+ # Collection#insert.
267
+ def insert_into_db(collection_name, objects)
268
+ @semaphore.synchronize {
269
+ objects.each { |o| send_to_db(InsertMessage.new(@name, collection_name, o)) }
270
+ }
271
+ end
272
+
273
+ def send_to_db(message)
274
+ @socket.print(message.buf.to_s)
275
+ end
276
+
277
+ def full_coll_name(collection_name)
278
+ "#{@name}.#{collection_name}"
279
+ end
280
+
281
+ # Return +true+ if +doc+ contains an 'ok' field with the value 1.
282
+ def ok?(doc)
283
+ ok = doc['ok']
284
+ ok.kind_of?(Numeric) && ok.to_i == 1
285
+ end
286
+
287
+ # DB commands need to be ordered, so selector must be an OrderedHash
288
+ # (or a Hash with only one element). What DB commands really need is
289
+ # that the "command" key be first.
290
+ #
291
+ # Do not call this. Intended for driver use only.
292
+ def db_command(selector)
293
+ if !selector.kind_of?(OrderedHash)
294
+ if !selector.kind_of?(Hash) || selector.keys.length > 1
295
+ raise "db_command must be given an OrderedHash when there is more than one key"
296
+ end
297
+ end
298
+
299
+ q = Query.new(selector)
300
+ q.number_to_return = 1
301
+ query(SYSTEM_COMMAND_COLLECTION, q).next_object
302
+ end
303
+
304
+ end
305
+ end
306
+ end
307
+ end