rdbi 0.9.1 → 1.1.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.
- checksums.yaml +7 -0
- data/.gemtest +0 -0
- data/History.txt +13 -0
- data/Manifest.txt +28 -0
- data/{README.rdoc → README.txt} +9 -9
- data/Rakefile +39 -133
- data/lib/rdbi.rb +44 -32
- data/lib/rdbi/database.rb +61 -20
- data/lib/rdbi/driver.rb +4 -1
- data/lib/rdbi/pool.rb +10 -0
- data/lib/rdbi/result.rb +94 -123
- data/lib/rdbi/schema.rb +8 -1
- data/lib/rdbi/statement.rb +22 -3
- data/lib/rdbi/types.rb +2 -0
- data/rdbi.gemspec +2 -4
- data/test/helper.rb +29 -1
- data/test/test_database.rb +95 -6
- data/test/test_pool.rb +11 -2
- data/test/test_rdbi.rb +54 -1
- data/test/test_result.rb +129 -7
- data/test/test_statement.rb +1 -1
- data/test/test_util.rb +2 -4
- metadata +201 -137
- data/.document +0 -5
- data/VERSION +0 -1
data/lib/rdbi/driver.rb
CHANGED
@@ -17,6 +17,9 @@ class RDBI::Driver
|
|
17
17
|
# the constructor.
|
18
18
|
def initialize(dbh_class, *args)
|
19
19
|
@dbh_class = dbh_class
|
20
|
+
if args.empty?
|
21
|
+
raise ArgumentError, "unable to connect without arguments"
|
22
|
+
end
|
20
23
|
@connect_args = [RDBI::Util.key_hash_as_symbols(args[0])]
|
21
24
|
end
|
22
25
|
|
@@ -25,7 +28,7 @@ class RDBI::Driver
|
|
25
28
|
# the RDBI::Database object, and sets the driver on the object to this
|
26
29
|
# current object for duplication / multiple creation.
|
27
30
|
#
|
28
|
-
def new_handle
|
31
|
+
def new_handle
|
29
32
|
dbh = @dbh_class.new(*@connect_args)
|
30
33
|
dbh.driver = self
|
31
34
|
return dbh
|
data/lib/rdbi/pool.rb
CHANGED
@@ -83,9 +83,11 @@ class RDBI::Pool
|
|
83
83
|
# Usage:
|
84
84
|
#
|
85
85
|
# Pool.new(:fart, [:SQLite3, :database => "/tmp/foo.db"])
|
86
|
+
# Pool.new(:quux, { :database => :SQLite3, :database => ":memory:" })
|
86
87
|
def initialize(name, connect_args, max=5)
|
87
88
|
@handles = []
|
88
89
|
@connect_args = connect_args
|
90
|
+
munge_connect_args!
|
89
91
|
@max = max
|
90
92
|
@last_index = 0
|
91
93
|
@mutex = Mutex.new
|
@@ -225,6 +227,14 @@ class RDBI::Pool
|
|
225
227
|
end
|
226
228
|
|
227
229
|
protected
|
230
|
+
|
231
|
+
# reformat the connect arguments coming from certain external sources.
|
232
|
+
def munge_connect_args!
|
233
|
+
if @connect_args.kind_of?(Hash) and @connect_args.has_key?(:driver)
|
234
|
+
new_connect_args = [@connect_args.delete(:driver), @connect_args]
|
235
|
+
@connect_args = new_connect_args
|
236
|
+
end
|
237
|
+
end
|
228
238
|
|
229
239
|
#
|
230
240
|
# Add any ol' database handle. This is not for global consumption.
|
data/lib/rdbi/result.rb
CHANGED
@@ -46,12 +46,6 @@ class RDBI::Result
|
|
46
46
|
# The RDBI::Result::Driver currently associated with this Result.
|
47
47
|
attr_reader :driver
|
48
48
|
|
49
|
-
# The count of results (see RDBI::Result main documentation)
|
50
|
-
attr_reader :result_count
|
51
|
-
|
52
|
-
# The count of affected rows by a DML statement (see RDBI::Result main documentation)
|
53
|
-
attr_reader :affected_count
|
54
|
-
|
55
49
|
# The mapping of types for each positional argument in the Result.
|
56
50
|
attr_reader :type_hash
|
57
51
|
|
@@ -84,18 +78,32 @@ class RDBI::Result
|
|
84
78
|
def initialize(sth, binds, data, schema, type_hash)
|
85
79
|
@schema = schema
|
86
80
|
@data = data
|
87
|
-
@
|
88
|
-
@affected_count = data.affected_count
|
81
|
+
@affected_count = nil # computed on demand
|
89
82
|
@sth = sth
|
90
83
|
@binds = binds
|
91
84
|
@type_hash = type_hash
|
92
85
|
@driver = RDBI::Result::Driver::Array
|
93
|
-
@
|
86
|
+
@result_driver = nil
|
94
87
|
|
95
88
|
configure_rewindable
|
96
89
|
configure_driver(@driver)
|
90
|
+
|
91
|
+
RDBI::Util.upon_finalize!(self, @data, :finish)
|
97
92
|
end
|
98
93
|
|
94
|
+
# The count of results (see RDBI::Result main documentation)
|
95
|
+
def result_count
|
96
|
+
# Non-rewindable cursors typically will give the number of rows
|
97
|
+
# fetched so far...
|
98
|
+
@data.size
|
99
|
+
end
|
100
|
+
|
101
|
+
# The count of affected rows by a DML statement (see RDBI::Result main documentation)
|
102
|
+
def affected_count
|
103
|
+
@affected_count ||= @data.affected_count
|
104
|
+
end
|
105
|
+
|
106
|
+
|
99
107
|
#
|
100
108
|
# Reload the result. This will:
|
101
109
|
#
|
@@ -108,17 +116,17 @@ class RDBI::Result
|
|
108
116
|
@data = res.instance_variable_get(:@data)
|
109
117
|
@type_hash = res.instance_variable_get(:@type_hash)
|
110
118
|
@schema = res.instance_variable_get(:@schema)
|
111
|
-
@
|
112
|
-
@affected_count = res.instance_variable_get(:@affected_count)
|
119
|
+
@affected_count = nil # recomputed on demand
|
113
120
|
|
114
121
|
configure_rewindable
|
115
122
|
end
|
116
123
|
|
117
124
|
#
|
118
|
-
# Iterator for Enumerable methods.
|
125
|
+
# Iterator for Enumerable methods. Yields a row at a time as translated
|
126
|
+
# by the current +driver+.
|
119
127
|
#
|
120
128
|
def each
|
121
|
-
|
129
|
+
while row = fetch(:next_row)
|
122
130
|
yield(row)
|
123
131
|
end
|
124
132
|
end
|
@@ -154,6 +162,7 @@ class RDBI::Result
|
|
154
162
|
#
|
155
163
|
# Replace the Result Driver. See RDBI::Result's main docs and
|
156
164
|
# RDBI::Result::Driver for more information on Result Drivers.
|
165
|
+
# Returns its receiver, permitting method chaining.
|
157
166
|
#
|
158
167
|
# You may pass:
|
159
168
|
#
|
@@ -179,6 +188,7 @@ class RDBI::Result
|
|
179
188
|
@data.rewindable_result = rr
|
180
189
|
@driver = driver_klass
|
181
190
|
configure_driver(@driver, *args)
|
191
|
+
self
|
182
192
|
end
|
183
193
|
|
184
194
|
#
|
@@ -229,53 +239,51 @@ class RDBI::Result
|
|
229
239
|
as(driver_klass, *args)
|
230
240
|
end
|
231
241
|
|
232
|
-
|
242
|
+
# fetch() has two significantly different return signatures:
|
243
|
+
#
|
244
|
+
# Returning a single row is nil upon EOF (:first, :last, inside #each)
|
245
|
+
# Returning a set of rows is [] upon EOF (all others)
|
246
|
+
|
247
|
+
raw, multiple_rows = case row_count
|
248
|
+
when :first, :last, :next_row
|
249
|
+
[@data.__send__(row_count), false]
|
250
|
+
when :all, :rest
|
251
|
+
[@data.__send__(row_count), true]
|
252
|
+
else
|
253
|
+
[@data.fetch(row_count), true]
|
254
|
+
end
|
255
|
+
|
256
|
+
return @result_driver.format_multiple_rows(raw) if multiple_rows
|
257
|
+
|
258
|
+
@result_driver.format_single_row(raw) if raw
|
259
|
+
|
260
|
+
# else nil -- :first, :last or #each at EOF
|
233
261
|
end
|
234
262
|
|
235
263
|
#
|
236
|
-
#
|
264
|
+
# Returns the first result in the set. Note that this may force an
|
265
|
+
# advance of the underlying cursor for non-rewindable ResultSets.
|
237
266
|
#
|
238
267
|
def first
|
239
|
-
|
268
|
+
fetch(:first)
|
240
269
|
end
|
241
270
|
|
242
271
|
#
|
243
|
-
#
|
272
|
+
# Returns the last result in the set. Note that this may exhaust the
|
273
|
+
# underlying cursor for non-rewindable ResultSets, as the driver advances
|
274
|
+
# to the end of the results to fetch the last row.
|
244
275
|
#
|
245
276
|
def last
|
246
|
-
|
277
|
+
fetch(:last)
|
247
278
|
end
|
248
279
|
|
249
280
|
alias read fetch
|
250
281
|
|
251
282
|
#
|
252
|
-
#
|
253
|
-
#
|
254
|
-
# a considerable amount of overlap.
|
255
|
-
#
|
256
|
-
# This is generally used by Result Drivers to transform results.
|
257
|
-
#
|
258
|
-
def raw_fetch(row_count)
|
259
|
-
final_res = case row_count
|
260
|
-
when :all
|
261
|
-
@data.all
|
262
|
-
when :rest
|
263
|
-
@data.rest
|
264
|
-
when :first
|
265
|
-
[@data.first]
|
266
|
-
when :last
|
267
|
-
[@data.last]
|
268
|
-
else
|
269
|
-
@data.fetch(row_count)
|
270
|
-
end
|
271
|
-
end
|
272
|
-
|
273
|
-
#
|
274
|
-
# This call finishes the result and the RDBI::Statement handle, scheduling
|
275
|
-
# any unpreserved data for garbage collection.
|
283
|
+
# This call finishes the result, scheduling any unpreserved data for
|
284
|
+
# garbage collection.
|
276
285
|
#
|
277
286
|
def finish
|
278
|
-
@sth.finish
|
279
287
|
@data.finish
|
280
288
|
@data = nil
|
281
289
|
@sth = nil
|
@@ -287,7 +295,7 @@ class RDBI::Result
|
|
287
295
|
protected
|
288
296
|
|
289
297
|
def configure_driver(driver_klass, *args)
|
290
|
-
@
|
298
|
+
@result_driver = driver_klass.new(self, *args)
|
291
299
|
end
|
292
300
|
|
293
301
|
def configure_rewindable
|
@@ -309,21 +317,21 @@ end
|
|
309
317
|
# == Creating a Result Driver
|
310
318
|
#
|
311
319
|
# A result driver typically inherits from RDBI::Result::Driver and implements
|
312
|
-
# at least
|
320
|
+
# at least two methods: +format_single_row+ and +format_multiple_rows+.
|
313
321
|
#
|
314
|
-
#
|
315
|
-
#
|
316
|
-
#
|
317
|
-
#
|
322
|
+
# +format_single_row+ is called with a single row fetched "raw" from the
|
323
|
+
# underlying driver, that is, with an array of variously typed elements. It
|
324
|
+
# is never called with a nil argument. +format_multiple_rows+ is called with
|
325
|
+
# a possibly empty array of raw rows.
|
318
326
|
#
|
319
|
-
#
|
320
|
-
#
|
321
|
-
# to assist in type conversion. For performance reasons, RDBI chooses to
|
322
|
-
# convert on request instead of preemptively, so <b>it is the driver implementor's
|
323
|
-
# job to do any conversion</b>.
|
327
|
+
# Note that +format_multiple_rows+ could be implemented in terms of
|
328
|
+
# +format_single_row+. However, for performance reasons it is not.
|
324
329
|
#
|
325
|
-
#
|
326
|
-
# RDBI::
|
330
|
+
# Base class RDBI::Result::Driver additionally provides the method
|
331
|
+
# +convert_row+ to employ RDBI's type conversion facility (see RDBI::Type) for
|
332
|
+
# converting "raw" data elements to convenient ruby types. For performance
|
333
|
+
# reasons, RDBI converts on request instead of preemptively, so <b>it is the
|
334
|
+
# driver implementor's job to do any conversion</b>.
|
327
335
|
#
|
328
336
|
class RDBI::Result::Driver
|
329
337
|
|
@@ -336,48 +344,17 @@ class RDBI::Result::Driver
|
|
336
344
|
@result = result
|
337
345
|
end
|
338
346
|
|
339
|
-
|
340
|
-
|
341
|
-
# type converted array.
|
342
|
-
#
|
343
|
-
def fetch(row_count)
|
344
|
-
rows = RDBI::Util.format_results(row_count, (@result.raw_fetch(row_count) || []))
|
345
|
-
|
346
|
-
if rows.nil?
|
347
|
-
return rows
|
348
|
-
elsif [:first, :last].include?(row_count)
|
349
|
-
return convert_row(rows)
|
350
|
-
else
|
351
|
-
result = []
|
352
|
-
rows.each do |row|
|
353
|
-
result << convert_row(row)
|
354
|
-
end
|
355
|
-
end
|
356
|
-
return result
|
357
|
-
end
|
358
|
-
|
359
|
-
#
|
360
|
-
# Returns the first result in the set.
|
361
|
-
#
|
362
|
-
def first
|
363
|
-
return fetch(:first)
|
347
|
+
def format_single_row(raw)
|
348
|
+
convert_row(raw)
|
364
349
|
end
|
365
350
|
|
366
|
-
|
367
|
-
|
368
|
-
#
|
369
|
-
# +Warning+: Depending on your database and drivers, calling this could have
|
370
|
-
# serious memory and performance implications!
|
371
|
-
#
|
372
|
-
def last
|
373
|
-
return fetch(:last)
|
351
|
+
def format_multiple_rows(raw_rows)
|
352
|
+
raw_rows.collect { |rr| convert_row(rr) }
|
374
353
|
end
|
375
354
|
|
376
355
|
protected
|
377
356
|
|
378
357
|
def convert_row(row)
|
379
|
-
return [] if row.nil?
|
380
|
-
|
381
358
|
row.each_with_index do |x, i|
|
382
359
|
row[i] = RDBI::Type::Out.convert(x, @result.schema.columns[i], @result.type_hash)
|
383
360
|
end
|
@@ -418,12 +395,12 @@ class RDBI::Result::Driver::CSV < RDBI::Result::Driver
|
|
418
395
|
# FIXME columns from schema deal maybe?
|
419
396
|
end
|
420
397
|
|
421
|
-
def
|
422
|
-
|
423
|
-
|
424
|
-
|
425
|
-
|
426
|
-
|
398
|
+
def format_single_row(raw)
|
399
|
+
raw.to_csv
|
400
|
+
end
|
401
|
+
|
402
|
+
def format_multiple_rows(raw_rows)
|
403
|
+
raw_rows.inject("") do |accum, row| accum << row.to_csv end
|
427
404
|
end
|
428
405
|
end
|
429
406
|
|
@@ -444,28 +421,21 @@ class RDBI::Result::Driver::Struct < RDBI::Result::Driver
|
|
444
421
|
super
|
445
422
|
end
|
446
423
|
|
447
|
-
def
|
448
|
-
|
449
|
-
|
450
|
-
klass = ::Struct.new(*column_names)
|
451
|
-
|
452
|
-
structs = super
|
453
|
-
|
454
|
-
if [:first, :last].include?(row_count)
|
455
|
-
if structs
|
456
|
-
return klass.new(*structs)
|
457
|
-
else
|
458
|
-
return structs
|
459
|
-
end
|
460
|
-
end
|
424
|
+
def format_single_row(raw)
|
425
|
+
struct_klass.new(*super)
|
426
|
+
end
|
461
427
|
|
462
|
-
|
428
|
+
def format_multiple_rows(raw_rows)
|
429
|
+
super.collect { |row| struct_klass.new(*row) }
|
430
|
+
end
|
463
431
|
|
464
|
-
|
432
|
+
private
|
433
|
+
def struct_klass
|
434
|
+
@struct_klass ||= ::Struct.new(*@result.schema.columns.map(&:name))
|
465
435
|
end
|
466
436
|
end
|
467
437
|
|
468
|
-
#
|
438
|
+
#
|
469
439
|
# Yields YAML representations of rows, each as an array keyed by the column
|
470
440
|
# name (as symbol, not string).
|
471
441
|
#
|
@@ -496,16 +466,17 @@ class RDBI::Result::Driver::YAML < RDBI::Result::Driver
|
|
496
466
|
RDBI::Util.optional_require('yaml')
|
497
467
|
end
|
498
468
|
|
499
|
-
def
|
500
|
-
column_names
|
469
|
+
def format_single_row(raw)
|
470
|
+
::Hash[column_names.zip(raw)].to_yaml
|
471
|
+
end
|
501
472
|
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
473
|
+
def format_multiple_rows(raw_rows)
|
474
|
+
raw_rows.collect { |row| ::Hash[column_names.zip(row)] }.to_yaml
|
475
|
+
end
|
476
|
+
|
477
|
+
private
|
478
|
+
def column_names
|
479
|
+
@column_names ||= @result.schema.columns.map(&:name)
|
509
480
|
end
|
510
481
|
end
|
511
482
|
|
data/lib/rdbi/schema.rb
CHANGED
@@ -13,7 +13,8 @@ RDBI::Column = Struct.new(
|
|
13
13
|
:nullable,
|
14
14
|
:metadata,
|
15
15
|
:default,
|
16
|
-
:table
|
16
|
+
:table,
|
17
|
+
:primary_key
|
17
18
|
)
|
18
19
|
|
19
20
|
#
|
@@ -116,6 +117,12 @@ class RDBI::Column
|
|
116
117
|
#
|
117
118
|
# The table this column belongs to, if known, as symbol.
|
118
119
|
#
|
120
|
+
|
121
|
+
##
|
122
|
+
# :attr_reader: primary_key
|
123
|
+
#
|
124
|
+
# Is this column a primary key?
|
125
|
+
#
|
119
126
|
end
|
120
127
|
|
121
128
|
# vim: syntax=ruby ts=2 et sw=2 sts=2
|
data/lib/rdbi/statement.rb
CHANGED
@@ -82,6 +82,9 @@
|
|
82
82
|
# which itself is provided by the epoxy gem) is unkempt, SQL injection attacks
|
83
83
|
# may be possible.
|
84
84
|
#
|
85
|
+
|
86
|
+
require 'weakref'
|
87
|
+
|
85
88
|
class RDBI::Statement
|
86
89
|
# the RDBI::Database handle that created this statement.
|
87
90
|
attr_reader :dbh
|
@@ -140,9 +143,9 @@ class RDBI::Statement
|
|
140
143
|
@dbh = dbh
|
141
144
|
@finished = false
|
142
145
|
@input_type_map = self.class.input_type_map
|
146
|
+
@finish_block = nil
|
143
147
|
|
144
148
|
self.rewindable_result = dbh.rewindable_result
|
145
|
-
@dbh.open_statements[self.object_id] = self
|
146
149
|
end
|
147
150
|
|
148
151
|
def prep_finalizer(&block)
|
@@ -162,9 +165,15 @@ class RDBI::Statement
|
|
162
165
|
|
163
166
|
cursor, schema, type_map = new_execution(*binds)
|
164
167
|
cursor.rewindable_result = self.rewindable_result
|
165
|
-
|
168
|
+
r = RDBI::Result.new(self, binds, cursor, schema, type_map)
|
169
|
+
self.last_result = ::WeakRef.new(r)
|
170
|
+
r
|
166
171
|
end
|
167
172
|
|
173
|
+
#
|
174
|
+
# Execute the statement with the supplied binds. Intended for DDL and
|
175
|
+
# other statements which return no rows (e.g., INSERT, DELETE).
|
176
|
+
#
|
168
177
|
def execute_modification(*binds)
|
169
178
|
binds = pre_execute(*binds)
|
170
179
|
|
@@ -208,8 +217,18 @@ class RDBI::Statement
|
|
208
217
|
raise NoMethodError, "this method is not implemented in this driver"
|
209
218
|
end
|
210
219
|
|
220
|
+
##
|
221
|
+
#
|
222
|
+
# Executes the statement, returning the number of rows affected.
|
223
|
+
#
|
224
|
+
# Database drivers may override this method in their +Statement+
|
225
|
+
# subclasses for efficiency. This method is called when
|
226
|
+
# #execute_modification() is called for an RDBI::Statement or
|
227
|
+
# RDBI::Database object.
|
228
|
+
#
|
211
229
|
def new_modification(*binds)
|
212
|
-
|
230
|
+
cursor = new_execution(*binds)[0]
|
231
|
+
cursor.affected_count
|
213
232
|
end
|
214
233
|
|
215
234
|
protected
|