rails-dbi 0.1.0

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