rdbi 0.9.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|