rdbi 0.9.0

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,35 @@
1
+ #
2
+ # RDBI::Driver is the bootstrap handle to yield database connection
3
+ # (RDBI::Database) handles. It preserves the connection parameters and the
4
+ # desired database, and outside of yielding handles, does little else.
5
+ #
6
+ # As such, it is normally intended to be used by RDBI internally and (rarely
7
+ # by) Database drivers.
8
+ #
9
+ class RDBI::Driver
10
+ # connection arguments requested during initialization
11
+ attr_reader :connect_args
12
+ # Database driver class requested for initialization
13
+ attr_reader :dbh_class
14
+
15
+ # Initialize a new driver object. This accepts an RDBI::Database subclass as a
16
+ # class name (shorthand does not work here) and the arguments to pass into
17
+ # the constructor.
18
+ def initialize(dbh_class, *args)
19
+ @dbh_class = dbh_class
20
+ @connect_args = [RDBI::Util.key_hash_as_symbols(args[0])]
21
+ end
22
+
23
+ #
24
+ # This is a proxy method to construct RDBI::Database handles. It constructs
25
+ # the RDBI::Database object, and sets the driver on the object to this
26
+ # current object for duplication / multiple creation.
27
+ #
28
+ def new_handle
29
+ dbh = @dbh_class.new(*@connect_args)
30
+ dbh.driver = self
31
+ return dbh
32
+ end
33
+ end
34
+
35
+ # vim: syntax=ruby ts=2 et sw=2 sts=2
@@ -0,0 +1,236 @@
1
+ #
2
+ # RDBI::Pool - Connection Pooling.
3
+ #
4
+ # Pools are named resources that consist of N concurrent connections which all
5
+ # have the same properties. Many group actions can be performed on them, such
6
+ # as disconnecting the entire lot.
7
+ #
8
+ # RDBI::Pool itself has a global accessor, by way of +RDBI::Pool::[]+, that can
9
+ # access these pools by name. Alternatively, you may access them through the
10
+ # RDBI.pool interface.
11
+ #
12
+ # Pools are thread-safe and are capable of being resized without disconnecting
13
+ # the culled database handles.
14
+ #
15
+ class RDBI::Pool
16
+
17
+ @mutex = Mutex.new
18
+
19
+ class << self
20
+ include Enumerable
21
+
22
+ # Iterate each pool and get the name of the pool (as a symbol) and the
23
+ # value as a Pool object.
24
+ def each
25
+ @pools.each do |key, value|
26
+ yield(key, value)
27
+ end
28
+ end
29
+
30
+ # obtain the names of each pool.
31
+ def keys
32
+ @pools.keys
33
+ end
34
+
35
+ # obtain the pool objects of each pool.
36
+ def values
37
+ @pools.values
38
+ end
39
+
40
+ #
41
+ # Retrieves a pool object for the name, or nothing if it does not exist.
42
+ #
43
+ def [](name)
44
+ mutex.synchronize do
45
+ @pools ||= { }
46
+ @pools[name.to_sym]
47
+ end
48
+ end
49
+
50
+ #
51
+ # Sets the pool for the name. This is not recommended for end-user code.
52
+ #
53
+ def []=(name, value)
54
+ mutex.synchronize do
55
+ @pools ||= { }
56
+ @pools[name.to_sym] = value
57
+ end
58
+ end
59
+
60
+ def mutex
61
+ @mutex
62
+ end
63
+ end
64
+
65
+ include Enumerable
66
+
67
+ # a list of the pool handles for this object. Do not manipulate this directly.
68
+ attr_reader :handles
69
+ # the last index corresponding to the latest allocation request.
70
+ attr_reader :last_index
71
+ # the maximum number of items this pool can hold. should only be altered by resize.
72
+ attr_reader :max
73
+ # the Mutex for this pool.
74
+ attr_reader :mutex
75
+
76
+ #
77
+ # Creates a new pool.
78
+ #
79
+ # * name: the name of this pool, which will be used to find it in the global accessor.
80
+ # * connect_args: an array of arguments that would be passed to RDBI.connect, including the driver name.
81
+ # * max: the maximum number of connections to deal with.
82
+ #
83
+ # Usage:
84
+ #
85
+ # Pool.new(:fart, [:SQLite3, :database => "/tmp/foo.db"])
86
+ def initialize(name, connect_args, max=5)
87
+ @handles = []
88
+ @connect_args = connect_args
89
+ @max = max
90
+ @last_index = 0
91
+ @mutex = Mutex.new
92
+ self.class[name] = self
93
+ end
94
+
95
+ # Obtain each database handle in the pool.
96
+ def each
97
+ @handles.each { |dbh| yield dbh }
98
+ end
99
+
100
+ #
101
+ # Ping all database connections and average out the amount.
102
+ #
103
+ # Any disconnected handles will be reconnected before this operation
104
+ # starts.
105
+ def ping
106
+ reconnect_if_disconnected
107
+ mutex.synchronize do
108
+ @handles.inject(1) { |sum,dbh| sum + (dbh.ping || 1) } / @handles.size
109
+ end
110
+ end
111
+
112
+ #
113
+ # Unconditionally reconnect all database handles.
114
+ def reconnect
115
+ mutex.synchronize do
116
+ @handles.each { |dbh| dbh.reconnect }
117
+ end
118
+ end
119
+
120
+ #
121
+ # Only reconnect the database handles that have not been already connected.
122
+ def reconnect_if_disconnected
123
+ mutex.synchronize do
124
+ @handles.each do |dbh|
125
+ dbh.reconnect unless dbh.connected?
126
+ end
127
+ end
128
+ end
129
+
130
+ #
131
+ # Disconnect all database handles.
132
+ def disconnect
133
+ mutex.synchronize do
134
+ @handles.each(&:disconnect)
135
+ end
136
+ end
137
+
138
+ #
139
+ # Add a connection, connecting automatically with the connect arguments
140
+ # supplied to the constructor.
141
+ def add_connection
142
+ add(RDBI.connect(*@connect_args))
143
+ end
144
+
145
+ #
146
+ # Remove a specific connection. If this connection does not exist in the
147
+ # pool already, nothing will occur.
148
+ #
149
+ # This database object is *not* disconnected -- it is your responsibility
150
+ # to do so.
151
+ def remove(dbh)
152
+ mutex.synchronize do
153
+ @handles.reject! { |x| x.object_id == dbh.object_id }
154
+ end
155
+ end
156
+
157
+ #
158
+ # Resize the pool. If the new pool size is smaller, connections will be
159
+ # forcibly removed, preferring disconnected handles over connected ones.
160
+ #
161
+ # No database connections are disconnected.
162
+ #
163
+ # Returns the handles that were removed, if any.
164
+ #
165
+ def resize(max=5)
166
+ mutex.synchronize do
167
+ in_pool = @handles.select(&:connected?)
168
+
169
+ unless (in_pool.size >= max)
170
+ disconnected = @handles.select { |x| !x.connected? }
171
+ if disconnected.size > 0
172
+ in_pool += disconnected[0..(max - in_pool.size - 1)]
173
+ end
174
+ else
175
+ in_pool = in_pool[0..(max-1)]
176
+ end
177
+
178
+ rejected = @handles - in_pool
179
+
180
+ @max = max
181
+ @handles = in_pool
182
+ rejected
183
+ end
184
+ end
185
+
186
+ #
187
+ # Obtain a database handle from the pool. Ordering is round robin.
188
+ #
189
+ # A new connection may be created if it fills in the pool where a
190
+ # previously empty object existed. Additionally, if the current database
191
+ # handle is disconnected, it will be reconnected.
192
+ #
193
+ def get_dbh
194
+ mutex.synchronize do
195
+ if @last_index >= @max
196
+ @last_index = 0
197
+ end
198
+
199
+ # XXX this is longhand for "make sure it's connected before we hand it
200
+ # off"
201
+ if @handles[@last_index] and !@handles[@last_index].connected?
202
+ @handles[@last_index].reconnect
203
+ elsif !@handles[@last_index]
204
+ @handles[@last_index] = RDBI.connect(*@connect_args)
205
+ end
206
+
207
+ dbh = @handles[@last_index]
208
+ @last_index += 1
209
+ dbh
210
+ end
211
+ end
212
+
213
+ protected
214
+
215
+ #
216
+ # Add any ol' database handle. This is not for global consumption.
217
+ #
218
+ def add(dbh)
219
+ dbh = *MethLab.validate_array_params([RDBI::Database], [dbh])
220
+ raise dbh if dbh.kind_of?(Exception)
221
+
222
+ dbh = dbh[0] if dbh.kind_of?(Array)
223
+
224
+ mutex.synchronize do
225
+ if @handles.size >= @max
226
+ raise ArgumentError, "too many handles in this pool (max: #{@max})"
227
+ end
228
+
229
+ @handles << dbh
230
+ end
231
+
232
+ return self
233
+ end
234
+ end
235
+
236
+ # vim: syntax=ruby ts=2 et sw=2 sts=2
@@ -0,0 +1,402 @@
1
+ #
2
+ # RDBI::Result encapsulates results from a statement.
3
+ #
4
+ # Results in RDBI::Result are row-oriented and may be transformable by Result
5
+ # Drivers (RDBI::Result::Driver). They are fetched as a unit or in order.
6
+ #
7
+ # The RDBI::Result API is deliberately architected to loosely resemble that of
8
+ # IO or File.
9
+ #
10
+ # == Just give me the data!
11
+ #
12
+ # Have a peek at RDBI::Result#fetch.
13
+ #
14
+ # == Result Counts
15
+ #
16
+ # Multiple kinds of counts are represented in each result:
17
+ #
18
+ # * A count of the results provided
19
+ # * A count of the affected rows.
20
+ #
21
+ # To elaborate, the "affected rows" is a count of rows that were altered by the
22
+ # statement from a DML result such as +INSERT+ or +UPDATE+. In some cases,
23
+ # statements will both alter rows and yield results, which is why this value is
24
+ # not switched depending on the kind of statement.
25
+ #
26
+ # == Result Drivers
27
+ #
28
+ # Result drivers are subclasses of RDBI::Result::Driver that take the result as
29
+ # input and yield a transformed input: data structures such a hashes, or even
30
+ # wilder results such as CSV or JSON or YAML. Given the ability to sanely
31
+ # transform row-oriented input, result drivers effectively have the power to do
32
+ # anything.
33
+ #
34
+ # Accessing result drivers is as easy as using a secondary form of
35
+ # RDBI::Result#fetch or more explicitly with the RDBI::Result#as call.
36
+ #
37
+ class RDBI::Result
38
+ extend MethLab
39
+ include Enumerable
40
+
41
+ # The RDBI::Schema structure associated with this result.
42
+ attr_reader :schema
43
+
44
+ # The RDBI::Statement that yielded this result.
45
+ attr_reader :sth
46
+
47
+ # The RDBI::Result::Driver currently associated with this Result.
48
+ attr_reader :driver
49
+
50
+ # The count of results (see RDBI::Result main documentation)
51
+ attr_reader :result_count
52
+
53
+ # The count of affected rows by a DML statement (see RDBI::Result main documentation)
54
+ attr_reader :affected_count
55
+
56
+ # The mapping of types for each positional argument in the Result.
57
+ attr_reader :type_hash
58
+
59
+ # The binds used in the statement that yielded this Result.
60
+ attr_reader :binds
61
+
62
+ # FIXME async
63
+ inline(:complete, :complete?) { true }
64
+
65
+ ##
66
+ # :attr_reader: has_data
67
+ #
68
+ # Does this result have data?
69
+
70
+ ##
71
+ # :attr_reader: has_data?
72
+ #
73
+ # Does this result have data?
74
+ inline(:has_data, :has_data?) { @data.size > 0 }
75
+
76
+ #
77
+ # Creates a new RDBI::Result. Please refer to RDBI::Statement#new_execution
78
+ # for instructions on how this is typically used and how the contents are
79
+ # passed to the constructor.
80
+ #
81
+ def initialize(sth, binds, data, schema, type_hash)
82
+ @schema = schema
83
+ @data = data
84
+ @result_count = data.size
85
+ @affected_count = data.affected_count
86
+ @sth = sth
87
+ @binds = binds
88
+ @type_hash = type_hash
89
+ @mutex = Mutex.new
90
+ @driver = RDBI::Result::Driver::Array
91
+ @fetch_handle = nil
92
+ as(@driver)
93
+ end
94
+
95
+ #
96
+ # Reload the result. This will:
97
+ #
98
+ # * Execute the statement that yielded this result again, with the original binds
99
+ # * Replace the results and other attributes with the new results.
100
+ #
101
+ def reload
102
+ @data.finish
103
+ res = @sth.execute(*@binds)
104
+ @data = res.instance_variable_get(:@data)
105
+ @type_hash = res.instance_variable_get(:@type_hash)
106
+ @schema = res.instance_variable_get(:@schema)
107
+ @result_count = res.instance_variable_get(:@result_count)
108
+ @affected_count = res.instance_variable_get(:@affected_count)
109
+ end
110
+
111
+ #
112
+ # Iterator for Enumerable methods. Yields a row at a time.
113
+ #
114
+ def each
115
+ @data.each do |row|
116
+ yield(row)
117
+ end
118
+ end
119
+
120
+ #
121
+ # Reset the index.
122
+ #
123
+ def rewind
124
+ @data.rewind
125
+ end
126
+
127
+ #
128
+ # Coerce the underlying result to an array, fetching all values.
129
+ #
130
+ def coerce_to_array
131
+ @data.coerce_to_array
132
+ end
133
+
134
+ #
135
+ # :call-seq:
136
+ # as(String)
137
+ # as(Symbol)
138
+ # as(Class)
139
+ # as([Class, String, or Symbol], *driver_arguments)
140
+ #
141
+ # Replace the Result Driver. See RDBI::Result's main docs and
142
+ # RDBI::Result::Driver for more information on Result Drivers.
143
+ #
144
+ # You may pass:
145
+ #
146
+ # * A Symbol or String which is shorthand for loading from the
147
+ # RDBI::Result::Driver namespace -- for example: "CSV" will result in the
148
+ # class RDBI::Result::Driver::CSV.
149
+ # * A full class name.
150
+ #
151
+ # There are no naming requirements; the String/Symbol form is just shorthand
152
+ # for convention.
153
+ #
154
+ # Any additional arguments will be passed to the driver's constructor.
155
+ #
156
+ def as(driver_klass, *args)
157
+
158
+ driver_klass = RDBI::Util.class_from_class_or_symbol(driver_klass, RDBI::Result::Driver)
159
+
160
+ @data.rewind
161
+ @driver = driver_klass
162
+ @fetch_handle = driver_klass.new(self, *args)
163
+ end
164
+
165
+ #
166
+ # :call-seq:
167
+ # fetch()
168
+ # fetch(Integer)
169
+ # fetch(:first)
170
+ # fetch(:last)
171
+ # fetch(:all)
172
+ # fetch(:rest)
173
+ # fetch(amount, [Class, String, or Symbol], *driver_arguments)
174
+ #
175
+ # fetch is the way people will typically interact with this class. It yields
176
+ # some or all of the results depending on the arguments given. Additionally,
177
+ # it can be supplemented with the arguments passed to RDBI::Result#as to
178
+ # one-off select a result driver.
179
+ #
180
+ # The initial argument can be none or one of many options:
181
+ #
182
+ # * An Integer n requests n rows from the result and increments the index.
183
+ # * No argument uses an Integer count of 1.
184
+ # * :first yields the first row of the result, regardless of the index.
185
+ # * :last yields the last row of the result, regardless of the index.
186
+ # * :all yields the whole set of rows, regardless of the index.
187
+ # * :rest yields all the items that have not been fetched, determined by the index.
188
+ # * :first and :last return nil if there are no results. All others will
189
+ # return an empty array.
190
+ #
191
+ # == The index
192
+ #
193
+ # I bet you're wondering what that is now, right? Well, the index is
194
+ # essentially a running row count that is altered by certain fetch
195
+ # operations. This makes sequential fetches much simpler.
196
+ #
197
+ # The index is largely implemented by RDBI::Cursor (and Database Driver
198
+ # subclasses)
199
+ #
200
+ # Items that do not use the index do not affect it.
201
+ #
202
+ # Result Drivers will always rewind the index, as this implicates a "point of
203
+ # no return" state change. You may always return to the original driver you
204
+ # were using, but the index position will be lost.
205
+ #
206
+ # The default result driver is RDBI::Result::Driver::Array.
207
+ #
208
+ def fetch(row_count=1, driver_klass=nil, *args)
209
+ if driver_klass
210
+ as(driver_klass, *args)
211
+ end
212
+
213
+ @fetch_handle.fetch(row_count)
214
+ end
215
+
216
+ alias read fetch
217
+
218
+ #
219
+ # raw_fetch is a straight array fetch without driver interaction. If you
220
+ # think you need this, please still read the fetch documentation as there is
221
+ # a considerable amount of overlap.
222
+ #
223
+ # This is generally used by Result Drivers to transform results.
224
+ #
225
+ def raw_fetch(row_count)
226
+ final_res = case row_count
227
+ when :all
228
+ @data.all
229
+ when :rest
230
+ @data.rest
231
+ when :first
232
+ [@data.first]
233
+ when :last
234
+ [@data.last]
235
+ else
236
+ @data.fetch(row_count)
237
+ end
238
+ RDBI::Util.deep_copy(final_res)
239
+ end
240
+
241
+ #
242
+ # This call finishes the result and the RDBI::Statement handle, scheduling
243
+ # any unpreserved data for garbage collection.
244
+ #
245
+ def finish
246
+ @sth.finish
247
+ @data.finish
248
+ @data = nil
249
+ @sth = nil
250
+ @driver = nil
251
+ @binds = nil
252
+ @schema = nil
253
+ end
254
+ end
255
+
256
+ #
257
+ # A result driver is a transformative element for RDBI::Result. Its design
258
+ # could be loosely described as a "fancy decorator".
259
+ #
260
+ # Usage and purpose is covered in the main RDBI::Result documentation. This
261
+ # section will largely serve the purpose of helping those who wish to implement
262
+ # result drivers themselves.
263
+ #
264
+ # == Creating a Result Driver
265
+ #
266
+ # A result driver typically inherits from RDBI::Result::Driver and implements
267
+ # at least one method: +fetch+.
268
+ #
269
+ # This fetch is not RDBI::Result#fetch, and doesn't have the same call
270
+ # semantics. Instead, it takes a single argument, the +row_count+, and
271
+ # typically passes that to RDBI::Result#raw_fetch to get results to process. It
272
+ # then returns the data transformed.
273
+ #
274
+ # RDBI::Result::Driver additionally provides two methods, convert_row and
275
+ # convert_item, which leverage RDBI's type conversion facility (see RDBI::Type)
276
+ # to assist in type conversion. For performance reasons, RDBI chooses to
277
+ # convert on request instead of preemptively, so <b>it is the driver implementor's
278
+ # job to do any conversion</b>.
279
+ #
280
+ # If you wish to implement a constructor in your class, please see
281
+ # RDBI::Result::Driver.new.
282
+ #
283
+ class RDBI::Result::Driver
284
+
285
+ #
286
+ # Result driver constructor. This is the logic that associates the result
287
+ # driver for decoration over the result; if you wish to override this method,
288
+ # please call +super+ before performing your own operations.
289
+ #
290
+ def initialize(result, *args)
291
+ @result = result
292
+ @result.rewind
293
+ end
294
+
295
+ #
296
+ # Fetch the result with any transformations. The default is to present the
297
+ # type converted array.
298
+ #
299
+ def fetch(row_count)
300
+ ary = (@result.raw_fetch(row_count) || []).enum_for.with_index.map do |item, i|
301
+ convert_row(item)
302
+ end
303
+
304
+ RDBI::Util.format_results(row_count, ary)
305
+ end
306
+
307
+ # convert an entire row of data with the specified result map (see
308
+ # RDBI::Type)
309
+ def convert_row(row)
310
+ newrow = []
311
+ (row || []).each_with_index do |x, i|
312
+ newrow.push(convert_item(x, @result.schema.columns[i]))
313
+ end
314
+ return newrow
315
+ end
316
+
317
+ # convert a single item (row element) with the specified result map.
318
+ def convert_item(item, column)
319
+ RDBI::Type::Out.convert(item, column, @result.type_hash)
320
+ end
321
+ end
322
+
323
+ #
324
+ # This is the standard Array driver. If you are familiar with the typical
325
+ # results of a database layer similar to RDBI, these results should be very
326
+ # familiar.
327
+ #
328
+ # If you wish for named accessors, please see RDBI::Result::Driver::Struct.
329
+ #
330
+ class RDBI::Result::Driver::Array < RDBI::Result::Driver
331
+ end
332
+
333
+ #
334
+ # This driver yields CSV:
335
+ #
336
+ # dbh.execute("select foo, bar from my_table").fetch(:first, :CSV)
337
+ #
338
+ # Yields the contents of columns foo and bar in CSV format (a String).
339
+ #
340
+ # The +fastercsv+ gem on 1.8 is used, which is the canonical +csv+ library on
341
+ # 1.9. If you are using Ruby 1.8 and do not have this gem available and try to
342
+ # use this driver, the code will abort during driver construction.
343
+ #
344
+ class RDBI::Result::Driver::CSV < RDBI::Result::Driver
345
+ def initialize(result, *args)
346
+ super
347
+ if RUBY_VERSION =~ /^1.8/
348
+ RDBI::Util.optional_require('fastercsv')
349
+ else
350
+ require 'csv'
351
+ end
352
+ # FIXME columns from schema deal maybe?
353
+ end
354
+
355
+ def fetch(row_count)
356
+ csv_string = ""
357
+ @result.raw_fetch(row_count).each do |row|
358
+ csv_string << row.to_csv
359
+ end
360
+ return csv_string
361
+ end
362
+ end
363
+
364
+ #
365
+ # Yields Struct objects instead of arrays for the rows. What this means is that
366
+ # you will recieve a single array of Structs, each struct representing a row of
367
+ # the database.
368
+ #
369
+ # example:
370
+ #
371
+ # results = dbh.execute("select foo, bar from my_table").fetch(:all, :Struct)
372
+ #
373
+ # results[0].foo # first row, foo column
374
+ # results[10].bar # 11th row, bar column
375
+ #
376
+ class RDBI::Result::Driver::Struct < RDBI::Result::Driver
377
+ def initialize(result, *args)
378
+ super
379
+ end
380
+
381
+ def fetch(row_count)
382
+ column_names = @result.schema.columns.map(&:name)
383
+
384
+ klass = ::Struct.new(*column_names)
385
+
386
+ structs = super
387
+
388
+ if [:first, :last].include?(row_count)
389
+ if structs
390
+ return klass.new(*structs)
391
+ else
392
+ return structs
393
+ end
394
+ end
395
+
396
+ structs.collect! { |row| klass.new(*row) }
397
+
398
+ return RDBI::Util.format_results(row_count, structs)
399
+ end
400
+ end
401
+
402
+ # vim: syntax=ruby ts=2 et sw=2 sts=2