rails-dbi 0.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.
Files changed (45) hide show
  1. data/ChangeLog +3694 -0
  2. data/LICENSE +25 -0
  3. data/README +271 -0
  4. data/bin/dbi +518 -0
  5. data/bin/test_broken_dbi +37 -0
  6. data/build/Rakefile.dbi.rb +60 -0
  7. data/examples/test1.pl +39 -0
  8. data/examples/test1.rb +20 -0
  9. data/examples/xmltest.rb +8 -0
  10. data/lib/dbi/base_classes/database.rb +135 -0
  11. data/lib/dbi/base_classes/driver.rb +47 -0
  12. data/lib/dbi/base_classes/statement.rb +171 -0
  13. data/lib/dbi/base_classes.rb +12 -0
  14. data/lib/dbi/binary.rb +25 -0
  15. data/lib/dbi/columninfo.rb +107 -0
  16. data/lib/dbi/exceptions.rb +65 -0
  17. data/lib/dbi/handles/database.rb +241 -0
  18. data/lib/dbi/handles/driver.rb +60 -0
  19. data/lib/dbi/handles/statement.rb +408 -0
  20. data/lib/dbi/handles.rb +49 -0
  21. data/lib/dbi/row.rb +270 -0
  22. data/lib/dbi/sql/preparedstatement.rb +115 -0
  23. data/lib/dbi/sql.rb +22 -0
  24. data/lib/dbi/sql_type_constants.rb +75 -0
  25. data/lib/dbi/trace.rb +91 -0
  26. data/lib/dbi/types.rb +218 -0
  27. data/lib/dbi/typeutil.rb +109 -0
  28. data/lib/dbi/utils/date.rb +59 -0
  29. data/lib/dbi/utils/tableformatter.rb +112 -0
  30. data/lib/dbi/utils/time.rb +52 -0
  31. data/lib/dbi/utils/timestamp.rb +96 -0
  32. data/lib/dbi/utils/xmlformatter.rb +73 -0
  33. data/lib/dbi/utils.rb +60 -0
  34. data/lib/dbi.rb +337 -0
  35. data/test/dbi/tc_columninfo.rb +94 -0
  36. data/test/dbi/tc_date.rb +88 -0
  37. data/test/dbi/tc_dbi.rb +184 -0
  38. data/test/dbi/tc_row.rb +256 -0
  39. data/test/dbi/tc_sqlbind.rb +168 -0
  40. data/test/dbi/tc_statementhandle.rb +29 -0
  41. data/test/dbi/tc_time.rb +77 -0
  42. data/test/dbi/tc_timestamp.rb +142 -0
  43. data/test/dbi/tc_types.rb +268 -0
  44. data/test/ts_dbi.rb +15 -0
  45. metadata +132 -0
@@ -0,0 +1,241 @@
1
+ module DBI
2
+ # DatabaseHandle is the interface the consumer sees after connecting to the
3
+ # database via DBI.connect.
4
+ #
5
+ # It is strongly discouraged that DBDs inherit from this class directly;
6
+ # please inherit from the DBI::BaseDatabase instead.
7
+ #
8
+ # Note: almost all methods in this class will raise InterfaceError if the
9
+ # database is not connected.
10
+ class DatabaseHandle < Handle
11
+
12
+ attr_accessor :last_statement
13
+ attr_accessor :raise_error
14
+
15
+ # This is the driver name as supplied by the DBD's driver_name method.
16
+ # Its primary utility is in DBI::TypeUtil#convert.
17
+ def driver_name
18
+ return @driver_name.dup if @driver_name
19
+ return nil
20
+ end
21
+
22
+ # Assign the driver name. This can be leveraged to create custom type
23
+ # management via DBI::TypeUtil#convert.
24
+ def driver_name=(name)
25
+ @driver_name = name
26
+ @driver_name.freeze
27
+ end
28
+
29
+ #
30
+ # Boolean if we are still connected to the database. See #ping.
31
+ #
32
+ def connected?
33
+ not @handle.nil?
34
+ end
35
+
36
+ #
37
+ # Disconnect from the database. Will raise InterfaceError if this was
38
+ # already done prior.
39
+ #
40
+ def disconnect
41
+ sanity_check
42
+ @handle.disconnect
43
+ @handle = nil
44
+ end
45
+
46
+ #
47
+ # Prepare a StatementHandle and return it. If given a block, it will
48
+ # supply that StatementHandle as the first argument to the block, and
49
+ # BaseStatement#finish it when the block is done executing.
50
+ #
51
+ def prepare(stmt)
52
+ sanity_check(stmt)
53
+ @last_statement = stmt
54
+ sth = StatementHandle.new(@handle.prepare(stmt), false, true, @convert_types)
55
+ # FIXME trace sth.trace(@trace_mode, @trace_output)
56
+ sth.dbh = self
57
+ sth.raise_error = raise_error
58
+
59
+ if block_given?
60
+ begin
61
+ yield sth
62
+ ensure
63
+ sth.finish unless sth.finished?
64
+ end
65
+ else
66
+ return sth
67
+ end
68
+ end
69
+
70
+ #
71
+ # Prepare and execute a statement. It has block semantics equivalent to #prepare.
72
+ #
73
+ def execute(stmt, *bindvars)
74
+ sanity_check(stmt)
75
+
76
+ @last_statement = stmt
77
+ if @convert_types
78
+ bindvars = DBI::Utils::ConvParam.conv_param(driver_name, *bindvars)
79
+ end
80
+
81
+ sth = StatementHandle.new(@handle.execute(stmt, *bindvars), true, true, @convert_types, true)
82
+ # FIXME trace sth.trace(@trace_mode, @trace_output)
83
+ sth.dbh = self
84
+ sth.raise_error = raise_error
85
+
86
+ if block_given?
87
+ begin
88
+ yield sth
89
+ ensure
90
+ sth.finish unless sth.finished?
91
+ end
92
+ else
93
+ return sth
94
+ end
95
+ end
96
+
97
+ #
98
+ # Perform a statement. This goes straight to the DBD's implementation
99
+ # of #do (and consequently, BaseDatabase#do), and does not work like
100
+ # #execute and #prepare. Should return a row modified count.
101
+ #
102
+ def do(stmt, *bindvars)
103
+ sanity_check(stmt)
104
+
105
+ @last_statement = stmt
106
+ @handle.do(stmt, *DBI::Utils::ConvParam.conv_param(driver_name, *bindvars))
107
+ end
108
+
109
+ #
110
+ # Executes a statement and returns the first row from the result.
111
+ #
112
+ def select_one(stmt, *bindvars)
113
+ sanity_check(stmt)
114
+ row = nil
115
+ execute(stmt, *bindvars) do |sth|
116
+ row = sth.fetch
117
+ end
118
+ row
119
+ end
120
+
121
+ #
122
+ # Executes a statement and returns all rows from the result. If a block
123
+ # is given, it is executed for each row.
124
+ #
125
+ def select_all(stmt, *bindvars, &p)
126
+ sanity_check(stmt)
127
+ rows = nil
128
+ execute(stmt, *bindvars) do |sth|
129
+ if block_given?
130
+ sth.each(&p)
131
+ else
132
+ rows = sth.fetch_all
133
+ end
134
+ end
135
+ return rows
136
+ end
137
+
138
+ #
139
+ # Return the name of the database we are connected to.
140
+ #
141
+ def database_name
142
+ sanity_check
143
+ @handle.database_name
144
+ end
145
+
146
+ #
147
+ # Return the tables available to this DatabaseHandle as an array of strings.
148
+ #
149
+ def tables
150
+ sanity_check
151
+ @handle.tables
152
+ end
153
+
154
+ #
155
+ # Returns the columns of the provided table as an array of ColumnInfo
156
+ # objects. See BaseDatabase#columns for the minimum parameters that
157
+ # this method must provide.
158
+ #
159
+ def columns( table )
160
+ sanity_check
161
+ @handle.columns( table ).collect {|col| ColumnInfo.new(col) }
162
+ end
163
+
164
+ #
165
+ # Attempt to establish if the database is still connected. While
166
+ # #connected? returns the state the DatabaseHandle thinks is true, this
167
+ # is an active operation that will contact the database.
168
+ #
169
+ def ping
170
+ sanity_check
171
+ @handle.ping
172
+ end
173
+
174
+ #
175
+ # Attempt to escape the value, rendering it suitable for inclusion in a SQL statement.
176
+ #
177
+ def quote(value)
178
+ sanity_check
179
+ @handle.quote(value)
180
+ end
181
+
182
+ #
183
+ # Force a commit to the database immediately.
184
+ #
185
+ def commit
186
+ sanity_check
187
+ @handle.commit
188
+ end
189
+
190
+ #
191
+ # Force a rollback to the database immediately.
192
+ #
193
+ def rollback
194
+ sanity_check
195
+ @handle.rollback
196
+ end
197
+
198
+ #
199
+ # Commits, runs the block provided, yielding the DatabaseHandle as it's
200
+ # argument. If an exception is raised through the block, rollback occurs.
201
+ # Otherwise, commit occurs.
202
+ #
203
+ def transaction
204
+ sanity_check
205
+ raise InterfaceError, "No block given" unless block_given?
206
+
207
+ commit
208
+ begin
209
+ yield self
210
+ commit
211
+ rescue Exception
212
+ rollback
213
+ raise
214
+ end
215
+ end
216
+
217
+ # Get an attribute from the DatabaseHandle.
218
+ def [] (attr)
219
+ sanity_check
220
+ @handle[attr]
221
+ end
222
+
223
+ # Set an attribute on the DatabaseHandle.
224
+ def []= (attr, val)
225
+ sanity_check
226
+ @handle[attr] = val
227
+ end
228
+
229
+ protected
230
+
231
+ def sanity_check(stmt=nil)
232
+ raise InterfaceError, "Database connection was already closed!" if @handle.nil?
233
+ check_statement(stmt) if stmt
234
+ end
235
+
236
+ # basic sanity checks for statements
237
+ def check_statement(stmt)
238
+ raise InterfaceError, "Statement is empty, or contains nothing but whitespace" if stmt !~ /\S/
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,60 @@
1
+ module DBI
2
+ # DriverHandles, while not directly exposed, are essentially the backend
3
+ # for the facade that many DBI root-level methods communicate with.
4
+ class DriverHandle < Handle
5
+
6
+ attr_writer :driver_name
7
+
8
+ # Connect to the database. The DSN will have been parsed at this point
9
+ # and the named parameters should need no explanation.
10
+ #
11
+ # If a block is provided to DBI.connect, the connected DatabaseHandle
12
+ # will be provided as the first argument to the block, and the
13
+ # DatabaseHandle will be disconnected upon block exit.
14
+ #
15
+ def connect(db_args, user, auth, params)
16
+
17
+ user = @handle.default_user[0] if user.nil?
18
+ auth = @handle.default_user[1] if auth.nil?
19
+
20
+ # TODO: what if only one of them is nil?
21
+ #if user.nil? and auth.nil? then
22
+ # user, auth = @handle.default_user
23
+ #end
24
+
25
+ params ||= {}
26
+ new_params = @handle.default_attributes
27
+ params.each {|k,v| new_params[k] = v}
28
+
29
+ if params.has_key?(:_convert_types)
30
+ @convert_types = params[:_convert_types]
31
+ end
32
+
33
+ db = @handle.connect(db_args, user, auth, new_params)
34
+ dbh = DatabaseHandle.new(db, @convert_types)
35
+ # FIXME trace
36
+ # dbh.trace(@trace_mode, @trace_output)
37
+ dbh.driver_name = @driver_name
38
+
39
+ if block_given?
40
+ begin
41
+ yield dbh
42
+ ensure
43
+ dbh.disconnect if dbh.connected?
44
+ end
45
+ else
46
+ return dbh
47
+ end
48
+ end
49
+
50
+ # See BaseDriver#data_sources.
51
+ def data_sources
52
+ @handle.data_sources
53
+ end
54
+
55
+ # See BaseDriver#disconnect_all.
56
+ def disconnect_all
57
+ @handle.disconnect_all
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,408 @@
1
+ module DBI
2
+ #
3
+ # StatementHandle is the interface the consumer sees after successfully
4
+ # issuing a DatabaseHandle#prepare. They may also be exposed through other
5
+ # methods that send statements to the database.
6
+ #
7
+ # Almost all methods in this class will raise InterfaceError if the
8
+ # statement is already finished.
9
+ #
10
+ class StatementHandle < Handle
11
+
12
+ include Enumerable
13
+
14
+ attr_accessor :dbh
15
+ attr_accessor :raise_error
16
+
17
+ def initialize(handle, fetchable=false, prepared=true, convert_types=true, executed=false)
18
+ super(handle)
19
+ @fetchable = fetchable
20
+ @prepared = prepared # only false if immediate execute was used
21
+ @executed = executed # only true if the statement was already executed.
22
+ @cols = nil
23
+ @coltypes = nil
24
+ @convert_types = convert_types
25
+
26
+ if @fetchable
27
+ @row = DBI::Row.new(column_names, column_types, nil, @convert_types)
28
+ else
29
+ @row = nil
30
+ end
31
+ end
32
+
33
+ # Returns true if the StatementHandle has had #finish called on it,
34
+ # explicitly or otherwise.
35
+ def finished?
36
+ @handle.nil?
37
+ end
38
+
39
+ # Returns true if the statement is believed to return data upon #fetch.
40
+ #
41
+ # The current reliability of this (and the concept in general) is
42
+ # suspect.
43
+ def fetchable?
44
+ @fetchable
45
+ end
46
+
47
+ #
48
+ # Instruct successive calls to #fetch to cast the type returned into
49
+ # `type`, for row position `pos`. Like all bind_* calls, `pos` indexes
50
+ # starting at 1.
51
+ #
52
+ # `type` is an object with the DBI::Type calling convention.
53
+ #
54
+ # This call must be called after #execute has successfully ran,
55
+ # otherwise it will raise InterfaceError.
56
+ #
57
+ # Example:
58
+ # # `foo` is an integer and this statement will return two rows.
59
+ # sth = dbh.prepare("select foo from bar")
60
+ # # would raise InterfaceError if called here
61
+ # sth.execute
62
+ #
63
+ # sth.bind_coltype(1, DBI::Type::Varchar)
64
+ # # would normally use DBI::Type::Integer and return a Fixnum. We'll make it a string.
65
+ # sth.fetch => ["1"]
66
+ #
67
+ # # Here we coerce it to Float.
68
+ # sth.bind_coltype(1, DBI::Type::Float)
69
+ # sth.fetch => [1.0]
70
+ # sth.finish
71
+ #
72
+ def bind_coltype(pos, type)
73
+ sanity_check({:prepared => true, :executed => true})
74
+
75
+ coltypes = column_types
76
+
77
+ if (pos - 1) < 0
78
+ raise InterfaceError, "bind positions index starting at 1"
79
+ end
80
+
81
+ coltypes[pos-1] = type
82
+ @row = DBI::Row.new(column_names, coltypes, nil, @convert_types)
83
+ end
84
+
85
+ #
86
+ # Just like BaseStatement#bind_param, but will attempt to convert the
87
+ # type if it's supposed to, adhering to the DBD's current ruleset.
88
+ #
89
+ def bind_param(param, value, attribs=nil)
90
+ sanity_check({ :prepared => true })
91
+
92
+ if @convert_types
93
+ value = DBI::Utils::ConvParam.conv_param(dbh.driver_name, value)[0]
94
+ end
95
+
96
+ @handle.bind_param(param, value, attribs)
97
+ end
98
+
99
+
100
+ # Execute the statement.
101
+ #
102
+ # This generally means that the statement will be sent to the database
103
+ # and some form of result cursor will be obtained, but is ultimately
104
+ # driver-dependent.
105
+ #
106
+ # If arguments are supplied, these are fed to #bind_param.
107
+ def execute(*bindvars)
108
+ cancel # cancel before
109
+ sanity_check({:prepared => true })
110
+
111
+ if @convert_types
112
+ bindvars = DBI::Utils::ConvParam.conv_param(dbh.driver_name, *bindvars)
113
+ end
114
+
115
+ @handle.bind_params(*bindvars)
116
+ @handle.execute
117
+ @fetchable = true
118
+ @executed = true
119
+
120
+ # TODO:?
121
+ #if @row.nil?
122
+ @row = DBI::Row.new(column_names, column_types, nil, @convert_types)
123
+ #end
124
+ return nil
125
+ end
126
+
127
+ #
128
+ # Finish the statement, causing the database to release all assets
129
+ # related to it (any result cursors, normally).
130
+ #
131
+ # StatementHandles that have already been finished will normally be
132
+ # inoperable and unavailable for further use.
133
+ #
134
+ def finish
135
+ sanity_check
136
+ @handle.finish
137
+ @handle = nil
138
+ end
139
+
140
+ #
141
+ # Cancel the query, closing any open result cursors and truncating any result sets.
142
+ #
143
+ # The difference between this and #finish is that cancelled statements
144
+ # may be re-executed.
145
+ #
146
+ def cancel
147
+ sanity_check
148
+ @handle.cancel if @fetchable
149
+ @fetchable = false
150
+ end
151
+
152
+ #
153
+ # Obtains the column names for this query as an array.
154
+ #
155
+ def column_names
156
+ sanity_check
157
+ return @cols unless @cols.nil?
158
+ @cols = @handle.column_info.collect {|col| col['name'] }
159
+ end
160
+
161
+ #
162
+ # Obtain the type mappings for the columns in this query based on
163
+ # ColumnInfo data on the query.
164
+ #
165
+ # The result will be a position-dependent array of objects that conform
166
+ # to the DBI::Type calling syntax.
167
+ #
168
+ def column_types
169
+ sanity_check
170
+ return @coltypes unless @coltypes.nil?
171
+ @coltypes = @handle.column_info.collect do |col|
172
+ if col['dbi_type']
173
+ col['dbi_type']
174
+ else
175
+ DBI::TypeUtil.type_name_to_module(col['type_name'])
176
+ end
177
+ end
178
+ end
179
+
180
+ #
181
+ # See BaseStatement#column_info.
182
+ #
183
+ def column_info
184
+ sanity_check
185
+ @handle.column_info.collect {|col| ColumnInfo.new(col) }
186
+ end
187
+
188
+ #
189
+ # Should return the row modified count as the result of statement execution.
190
+ #
191
+ # However, some low-level drivers do not supply this information or
192
+ # supply misleading information (> 0 rows for read-only select
193
+ # statements, f.e.)
194
+ #
195
+ def rows
196
+ sanity_check
197
+ @handle.rows
198
+ end
199
+
200
+
201
+ #
202
+ # See BaseStatement#fetch.
203
+ #
204
+ # fetch can also take a block which will be applied to each row in a
205
+ # similar fashion to Enumerable#collect. See #each.
206
+ #
207
+ def fetch(&p)
208
+ sanity_check({ :fetchable => true, :prepared => true, :executed => true })
209
+
210
+ if block_given?
211
+ while (res = @handle.fetch) != nil
212
+ @row = @row.dup
213
+ @row.set_values(res)
214
+ yield @row
215
+ end
216
+ @handle.cancel
217
+ @fetchable = false
218
+ return nil
219
+ else
220
+ res = @handle.fetch
221
+ if res.nil?
222
+ @handle.cancel
223
+ @fetchable = false
224
+ else
225
+ @row = @row.dup
226
+ @row.set_values(res)
227
+ res = @row
228
+ end
229
+ return res
230
+ end
231
+ end
232
+
233
+ #
234
+ # Synonym for #fetch with a block.
235
+ #
236
+ def each(&p)
237
+ sanity_check({:fetchable => true, :prepared => true, :executed => true})
238
+ raise InterfaceError, "No block given" unless block_given?
239
+
240
+ fetch(&p)
241
+ end
242
+
243
+ #
244
+ # Similar to #fetch, but returns Array of Array instead of Array of
245
+ # DBI::Row objects (and therefore does not perform type mapping). This
246
+ # is basically a way to get the raw data from the DBD.
247
+ #
248
+ def fetch_array
249
+ sanity_check({:fetchable => true, :prepared => true, :executed => true})
250
+
251
+ if block_given?
252
+ while (res = @handle.fetch) != nil
253
+ yield res
254
+ end
255
+ @handle.cancel
256
+ @fetchable = false
257
+ return nil
258
+ else
259
+ res = @handle.fetch
260
+ if res.nil?
261
+ @handle.cancel
262
+ @fetchable = false
263
+ end
264
+ return res
265
+ end
266
+ end
267
+
268
+ #
269
+ # Map the columns and results into an Array of Hash resultset.
270
+ #
271
+ # No type conversion is performed here. Expect this to change in 0.6.0.
272
+ #
273
+ def fetch_hash
274
+ sanity_check({:fetchable => true, :prepared => true, :executed => true})
275
+
276
+ cols = column_names
277
+
278
+ if block_given?
279
+ while (row = @handle.fetch) != nil
280
+ hash = {}
281
+ row.each_with_index {|v,i| hash[cols[i]] = v}
282
+ yield hash
283
+ end
284
+ @handle.cancel
285
+ @fetchable = false
286
+ return nil
287
+ else
288
+ row = @handle.fetch
289
+ if row.nil?
290
+ @handle.cancel
291
+ @fetchable = false
292
+ return nil
293
+ else
294
+ hash = {}
295
+ row.each_with_index {|v,i| hash[cols[i]] = v}
296
+ return hash
297
+ end
298
+ end
299
+ end
300
+
301
+ #
302
+ # Fetch `cnt` rows. Result is array of DBI::Row
303
+ #
304
+ def fetch_many(cnt)
305
+ sanity_check({:fetchable => true, :prepared => true, :executed => true})
306
+
307
+ cols = column_names
308
+ rows = @handle.fetch_many(cnt)
309
+ if rows.nil? or rows.empty?
310
+ @handle.cancel
311
+ @fetchable = false
312
+ return []
313
+ else
314
+ return rows.collect{|r| tmp = @row.dup; tmp.set_values(r); tmp }
315
+ end
316
+ end
317
+
318
+ #
319
+ # Fetch the entire result set. Result is array of DBI::Row.
320
+ #
321
+ def fetch_all
322
+ sanity_check({:fetchable => true, :prepared => true, :executed => true})
323
+
324
+ cols = column_names
325
+ fetched_rows = []
326
+
327
+ begin
328
+ while row = fetch do
329
+ fetched_rows.push(row)
330
+ end
331
+ rescue Exception
332
+ end
333
+
334
+ @handle.cancel
335
+ @fetchable = false
336
+
337
+ return fetched_rows
338
+ end
339
+
340
+ #
341
+ # See BaseStatement#fetch_scroll.
342
+ #
343
+ def fetch_scroll(direction, offset=1)
344
+ sanity_check({:fetchable => true, :prepared => true, :executed => true})
345
+
346
+ row = @handle.fetch_scroll(direction, offset)
347
+ if row.nil?
348
+ #@handle.cancel
349
+ #@fetchable = false
350
+ return nil
351
+ else
352
+ @row.set_values(row)
353
+ return @row
354
+ end
355
+ end
356
+
357
+ # Get an attribute from the StatementHandle object.
358
+ def [] (attr)
359
+ sanity_check
360
+ @handle[attr]
361
+ end
362
+
363
+ # Set an attribute on the StatementHandle object.
364
+ def []= (attr, val)
365
+ sanity_check
366
+ @handle[attr] = val
367
+ end
368
+
369
+ protected
370
+
371
+ def sanity_check(params={})
372
+ raise InterfaceError, "Statement was already closed!" if @handle.nil?
373
+
374
+ params.each_key do |key|
375
+ case key
376
+ when :fetchable
377
+ check_fetchable
378
+ when :executed
379
+ check_executed
380
+ when :prepared
381
+ check_prepared
382
+ when :statement
383
+ check_statement(params[:statement])
384
+ end
385
+ end
386
+ end
387
+
388
+ def check_prepared
389
+ raise InterfaceError, "Statement wasn't prepared before." unless @prepared
390
+ end
391
+
392
+ def check_fetchable
393
+ if !@fetchable and @raise_error
394
+ raise InterfaceError, "Statement does not have any data for fetching."
395
+ end
396
+ end
397
+
398
+ def check_executed
399
+ raise InterfaceError, "Statement hasn't been executed yet." unless @executed
400
+ end
401
+
402
+ # basic sanity checks for statements
403
+ def check_statement(stmt)
404
+ raise InterfaceError, "Statement is empty, or contains nothing but whitespace" if stmt !~ /\S/
405
+ end
406
+
407
+ end # class StatementHandle
408
+ end