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.
@@ -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
@@ -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.
@@ -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
- @result_count = data.size
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
- @fetch_handle = nil
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
- @result_count = res.instance_variable_get(:@result_count)
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. Yields a row at a time.
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
- @data.each do |row|
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
- @fetch_handle.fetch(row_count)
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
- # returns the first result in the set.
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
- @data.first
268
+ fetch(:first)
240
269
  end
241
270
 
242
271
  #
243
- # returns the last result in the set.
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
- @data.last
277
+ fetch(:last)
247
278
  end
248
279
 
249
280
  alias read fetch
250
281
 
251
282
  #
252
- # raw_fetch is a straight array fetch without driver interaction. If you
253
- # think you need this, please still read the fetch documentation as there is
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
- @fetch_handle = driver_klass.new(self, *args)
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 one method: +fetch+.
320
+ # at least two methods: +format_single_row+ and +format_multiple_rows+.
313
321
  #
314
- # This fetch is not RDBI::Result#fetch, and doesn't have the same call
315
- # semantics. Instead, it takes a single argument, the +row_count+, and
316
- # typically passes that to RDBI::Result#raw_fetch to get results to process. It
317
- # then returns the data transformed.
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
- # RDBI::Result::Driver additionally provides two methods, convert_row and
320
- # convert_item, which leverage RDBI's type conversion facility (see RDBI::Type)
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
- # If you wish to implement a constructor in your class, please see
326
- # RDBI::Result::Driver.new.
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
- # Fetch the result with any transformations. The default is to present the
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
- # Returns the last result in the set.
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 fetch(row_count)
422
- csv_string = ""
423
- @result.raw_fetch(row_count).each do |row|
424
- csv_string << row.to_csv
425
- end
426
- return csv_string
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 fetch(row_count)
448
- column_names = @result.schema.columns.map(&:name)
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
- structs.collect! { |row| klass.new(*row) }
428
+ def format_multiple_rows(raw_rows)
429
+ super.collect { |row| struct_klass.new(*row) }
430
+ end
463
431
 
464
- return RDBI::Util.format_results(row_count, structs)
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 fetch(row_count)
500
- column_names = @result.schema.columns.map(&:name)
469
+ def format_single_row(raw)
470
+ ::Hash[column_names.zip(raw)].to_yaml
471
+ end
501
472
 
502
- if [:first, :last].include?(row_count)
503
- Hash[column_names.zip(@result.raw_fetch(row_count)[0])].to_yaml
504
- else
505
- @result.raw_fetch(row_count).collect do |row|
506
- Hash[column_names.zip(row)]
507
- end.to_yaml
508
- end
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
 
@@ -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
@@ -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
- self.last_result = RDBI::Result.new(self, binds, cursor, schema, type_map)
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
- raise NoMethodError, "this method is not implemented in this driver"
230
+ cursor = new_execution(*binds)[0]
231
+ cursor.affected_count
213
232
  end
214
233
 
215
234
  protected