rdbi 0.9.1 → 1.1.0

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