ydbi 0.5.8 → 0.6.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 (86) hide show
  1. checksums.yaml +4 -4
  2. data/.envrc +3 -0
  3. data/.github/workflows/ruby.yml +65 -0
  4. data/.gitignore +19 -0
  5. data/ChangeLog +8 -0
  6. data/Gemfile +5 -0
  7. data/Rakefile +14 -0
  8. data/TODO +44 -0
  9. data/bench/bench.rb +79 -0
  10. data/build/rake_task_lib.rb +186 -0
  11. data/devenv.lock +228 -0
  12. data/devenv.nix +55 -0
  13. data/devenv.yaml +8 -0
  14. data/doc/DBD_SPEC.rdoc +88 -0
  15. data/doc/DBI_SPEC.rdoc +157 -0
  16. data/doc/homepage/contact.html +62 -0
  17. data/doc/homepage/development.html +124 -0
  18. data/doc/homepage/index.html +83 -0
  19. data/doc/homepage/ruby-dbi.css +91 -0
  20. data/lib/dbd/Mysql.rb +137 -0
  21. data/lib/dbd/ODBC.rb +89 -0
  22. data/lib/dbd/Pg.rb +189 -0
  23. data/lib/dbd/SQLite.rb +97 -0
  24. data/lib/dbd/SQLite3.rb +124 -0
  25. data/lib/dbd/mysql/database.rb +405 -0
  26. data/lib/dbd/mysql/driver.rb +125 -0
  27. data/lib/dbd/mysql/statement.rb +188 -0
  28. data/lib/dbd/odbc/database.rb +128 -0
  29. data/lib/dbd/odbc/driver.rb +38 -0
  30. data/lib/dbd/odbc/statement.rb +137 -0
  31. data/lib/dbd/pg/database.rb +508 -0
  32. data/lib/dbd/pg/exec.rb +47 -0
  33. data/lib/dbd/pg/statement.rb +160 -0
  34. data/lib/dbd/pg/tuples.rb +121 -0
  35. data/lib/dbd/pg/type.rb +209 -0
  36. data/lib/dbd/sqlite/database.rb +151 -0
  37. data/lib/dbd/sqlite/statement.rb +125 -0
  38. data/lib/dbd/sqlite3/database.rb +201 -0
  39. data/lib/dbd/sqlite3/statement.rb +78 -0
  40. data/lib/dbi/version.rb +1 -1
  41. data/prototypes/types2.rb +237 -0
  42. data/readme.md +3 -4
  43. data/setup.rb +1585 -0
  44. data/test/DBD_TESTS +50 -0
  45. data/test/TESTING +16 -0
  46. data/test/dbd/general/test_database.rb +206 -0
  47. data/test/dbd/general/test_statement.rb +325 -0
  48. data/test/dbd/general/test_types.rb +295 -0
  49. data/test/dbd/mysql/base.rb +26 -0
  50. data/test/dbd/mysql/down.sql +19 -0
  51. data/test/dbd/mysql/test_blob.rb +18 -0
  52. data/test/dbd/mysql/test_new_methods.rb +7 -0
  53. data/test/dbd/mysql/test_patches.rb +111 -0
  54. data/test/dbd/mysql/up.sql +28 -0
  55. data/test/dbd/odbc/base.rb +30 -0
  56. data/test/dbd/odbc/down.sql +19 -0
  57. data/test/dbd/odbc/test_new_methods.rb +12 -0
  58. data/test/dbd/odbc/test_ping.rb +10 -0
  59. data/test/dbd/odbc/test_statement.rb +44 -0
  60. data/test/dbd/odbc/test_transactions.rb +58 -0
  61. data/test/dbd/odbc/up.sql +33 -0
  62. data/test/dbd/postgresql/base.rb +31 -0
  63. data/test/dbd/postgresql/down.sql +31 -0
  64. data/test/dbd/postgresql/test_arrays.rb +179 -0
  65. data/test/dbd/postgresql/test_async.rb +121 -0
  66. data/test/dbd/postgresql/test_blob.rb +37 -0
  67. data/test/dbd/postgresql/test_bytea.rb +88 -0
  68. data/test/dbd/postgresql/test_ping.rb +10 -0
  69. data/test/dbd/postgresql/test_timestamp.rb +77 -0
  70. data/test/dbd/postgresql/test_transactions.rb +58 -0
  71. data/test/dbd/postgresql/testdbipg.rb +307 -0
  72. data/test/dbd/postgresql/up.sql +60 -0
  73. data/test/dbd/sqlite/base.rb +32 -0
  74. data/test/dbd/sqlite/test_database.rb +30 -0
  75. data/test/dbd/sqlite/test_driver.rb +68 -0
  76. data/test/dbd/sqlite/test_statement.rb +112 -0
  77. data/test/dbd/sqlite/up.sql +25 -0
  78. data/test/dbd/sqlite3/base.rb +32 -0
  79. data/test/dbd/sqlite3/test_database.rb +77 -0
  80. data/test/dbd/sqlite3/test_driver.rb +67 -0
  81. data/test/dbd/sqlite3/test_statement.rb +88 -0
  82. data/test/dbd/sqlite3/up.sql +33 -0
  83. data/test/dbi/tc_dbi.rb +1 -1
  84. data/test/ts_dbd.rb +136 -0
  85. data/ydbi.gemspec +25 -0
  86. metadata +148 -12
@@ -0,0 +1,121 @@
1
+ #
2
+ # Tuples is a class to represent result sets.
3
+ #
4
+ # Many of these methods are extremely similar to the methods that deal with
5
+ # result sets in DBI::BaseStatement and are wrapped by the StatementHandle.
6
+ # Unless you plan on working on this driver, these methods should never be
7
+ # called directly.
8
+ #
9
+ class DBI::DBD::Pg::Tuples
10
+
11
+ def initialize(db, pg_result)
12
+ @db = db
13
+ @pg_result = pg_result
14
+ @index = -1
15
+ @row = []
16
+ end
17
+
18
+ # See DBI::BaseStatement#column_info. Additional attributes:
19
+ #
20
+ # * array_of_type: True if this is actually an array of this type. In this
21
+ # case, +dbi_type+ will be the type authority for conversion.
22
+ #
23
+ def column_info
24
+ a = []
25
+ 0.upto(@pg_result.num_fields-1) do |i|
26
+ str = @pg_result.fname(i)
27
+
28
+ typeinfo = nil
29
+
30
+ begin
31
+ typmod = @pg_result.fmod(i)
32
+ rescue
33
+ end
34
+
35
+ if typmod and typ = @pg_result.ftype(i)
36
+ res = @db._exec("select format_type(#{typ}, #{typmod})")
37
+ typeinfo = DBI::DBD::Pg.parse_type(res[0].values[0])
38
+ end
39
+
40
+ map = @db.type_map[@pg_result.ftype(i)] || { }
41
+ h = { "name" => str }.merge(map)
42
+
43
+ if typeinfo
44
+ h["precision"] = typeinfo[:size]
45
+ h["scale"] = typeinfo[:decimal]
46
+ h["type"] = typeinfo[:type]
47
+ h["array_of_type"] = typeinfo[:array]
48
+
49
+ if typeinfo[:array]
50
+ h['dbi_type'] =
51
+ DBI::DBD::Pg::Type::Array.new(
52
+ DBI::TypeUtil.type_name_to_module(typeinfo[:type])
53
+ )
54
+ end
55
+ end
56
+
57
+ a.push h
58
+ end
59
+
60
+ return a
61
+ end
62
+
63
+ def fetchrow
64
+ @index += 1
65
+ if @index < @pg_result.num_tuples && @index >= 0
66
+ @row = Array.new
67
+ 0.upto(@pg_result.num_fields-1) do |x|
68
+ @row.push(@pg_result.getvalue(@index, x))
69
+ end
70
+ @row
71
+ else
72
+ nil
73
+ end
74
+ end
75
+
76
+ #
77
+ # Just don't use this method. It'll be fixed soon.
78
+ #
79
+ def fetch_scroll(direction, offset)
80
+ # Exact semantics aren't too closely defined. I attempted to follow the DBI:Mysql example.
81
+ case direction
82
+ when SQL_FETCH_NEXT
83
+ # Nothing special to do, besides the fetchrow
84
+ when SQL_FETCH_PRIOR
85
+ @index -= 2
86
+ when SQL_FETCH_FIRST
87
+ @index = -1
88
+ when SQL_FETCH_LAST
89
+ @index = @pg_result.num_tuples - 2
90
+ when SQL_FETCH_ABSOLUTE
91
+ # Note: if you go "out of range", all fetches will give nil until you get back
92
+ # into range, this doesn't raise an error.
93
+ @index = offset-1
94
+ when SQL_FETCH_RELATIVE
95
+ # Note: if you go "out of range", all fetches will give nil until you get back
96
+ # into range, this doesn't raise an error.
97
+ @index += offset - 1
98
+ else
99
+ raise NotSupportedError
100
+ end
101
+ self.fetchrow
102
+ end
103
+
104
+ #
105
+ # The number of rows returned.
106
+ #
107
+ def row_count
108
+ @pg_result.num_tuples
109
+ end
110
+
111
+ #
112
+ # The row processed count. This is analogue to DBI::StatementHandle#rows.
113
+ #
114
+ def rows_affected
115
+ @pg_result.cmdtuples
116
+ end
117
+
118
+ def finish
119
+ @pg_result.clear
120
+ end
121
+ end # Tuples
@@ -0,0 +1,209 @@
1
+ #
2
+ # Type Management for PostgreSQL-specific types.
3
+ #
4
+ # See DBI::Type and DBI::TypeUtil for more information.
5
+ #
6
+ module DBI::DBD::Pg::Type
7
+ #
8
+ # ByteA is a special escaped form of binary data, suitable for inclusion in queries.
9
+ #
10
+ # This class is an attempt to abstract that type so you do not have to
11
+ # concern yourself with the conversion issues.
12
+ #
13
+ class ByteA
14
+
15
+ attr_reader :original
16
+ attr_reader :escaped
17
+
18
+ #
19
+ # Build a new ByteA object.
20
+ #
21
+ # The data supplied is the unescaped binary data you wish to put in the
22
+ # database.
23
+ #
24
+ def initialize(obj)
25
+ @original = obj
26
+ @escaped = escape_bytea(obj)
27
+ @original.freeze
28
+ @escaped.freeze
29
+ end
30
+
31
+ #
32
+ # Escapes the supplied data. Has no effect on the object.
33
+ #
34
+ def escape_bytea(str)
35
+ PG::Connection.escape_bytea(str)
36
+ end
37
+
38
+ #
39
+ # Returns the original data.
40
+ #
41
+ def to_s
42
+ return @original.dup
43
+ end
44
+
45
+ #
46
+ # Class method to escape the data into ByteA format.
47
+ #
48
+ def self.escape_bytea(str)
49
+ self.new(str).escaped
50
+ end
51
+
52
+ #
53
+ # Class method to unescape the ByteA data and present it as a string.
54
+ #
55
+ def self.parse(obj)
56
+
57
+ return nil if obj.nil?
58
+
59
+ # FIXME there's a bug in the upstream 'pg' driver that does not
60
+ # properly decode bytea, leaving in an extra slash for each decoded
61
+ # character.
62
+ #
63
+ # Fix this for now, but beware that we'll have to unfix this as
64
+ # soon as they fix their end.
65
+ ret = PG::Connection.unescape_bytea(obj)
66
+
67
+ # XXX
68
+ # String#split does not properly create a full array if the the
69
+ # string ENDS in the split regex, unless this oddball -1 argument is supplied.
70
+ #
71
+ # Another way of saying this:
72
+ # if foo = "foo\\\\\" and foo.split(/\\\\/), the result will be
73
+ # ["foo"]. You can add as many delimiters to the end of the string
74
+ # as you'd like - the result is no different.
75
+ #
76
+
77
+ ret = ret.split(/\\\\/, -1).collect { |x| x.length > 0 ? x.gsub(/\\[0-7]{3}/) { |y| y[1..3].oct.chr } : "" }.join("\\")
78
+ ret.gsub!(/''/, "'")
79
+ return ret
80
+ end
81
+ end
82
+
83
+ #
84
+ # PostgreSQL arrays are simply a specification that sits on top of normal
85
+ # types. They have a specialized string grammar and this class facilitates
86
+ # converting that syntax and the types within those arrays.
87
+ #
88
+ class Array
89
+
90
+ attr_reader :base_type
91
+
92
+ #
93
+ # +base_type+ is a DBI::Type that is used to parse the inner types when
94
+ # a non-array one is found.
95
+ #
96
+ # For instance, if you had an array of integer, one would pass
97
+ # DBI::Type::Integer here.
98
+ #
99
+ def initialize(base_type)
100
+ @base_type = base_type
101
+ end
102
+
103
+ #
104
+ # Object method. Please note that this is different than most DBI::Type
105
+ # classes! One must initialize an Array object with an appropriate
106
+ # DBI::Type used to convert the indices of the array before this method
107
+ # can be called.
108
+ #
109
+ # Returns an appropriately converted array.
110
+ #
111
+ def parse(obj)
112
+ if obj.nil?
113
+ nil
114
+ elsif obj.index('{') == 0 and obj.rindex('}') == (obj.length - 1)
115
+ convert_array(obj)
116
+ else
117
+ raise "Not an array"
118
+ end
119
+ end
120
+
121
+ #
122
+ # Parse a PostgreSQL-Array output and convert into ruby array. This
123
+ # does the real parsing work.
124
+ #
125
+ def convert_array(str)
126
+
127
+ array_nesting = 0 # nesting level of the array
128
+ in_string = false # currently inside a quoted string ?
129
+ escaped = false # if the character is escaped
130
+ sbuffer = '' # buffer for the current element
131
+ result_array = ::Array.new # the resulting Array
132
+
133
+ str.each_byte { |char| # parse character by character
134
+ char = char.chr # we need the Character, not it's Integer
135
+
136
+ if escaped then # if this character is escaped, just add it to the buffer
137
+ sbuffer += char
138
+ escaped = false
139
+ next
140
+ end
141
+
142
+ case char # let's see what kind of character we have
143
+ #------------- {: beginning of an array ----#
144
+ when '{'
145
+ if in_string then # ignore inside a string
146
+ sbuffer += char
147
+ next
148
+ end
149
+
150
+ if array_nesting >= 1 then # if it's an nested array, defer for recursion
151
+ sbuffer += char
152
+ end
153
+ array_nesting += 1 # inside another array
154
+
155
+ #------------- ": string deliminator --------#
156
+ when '"'
157
+ in_string = !in_string
158
+
159
+ #------------- \: escape character, next is regular character #
160
+ when "\\" # single \, must be extra escaped in Ruby
161
+ if array_nesting > 1
162
+ sbuffer += char
163
+ else
164
+ escaped = true
165
+ end
166
+
167
+ #------------- ,: element separator ---------#
168
+ when ','
169
+ if in_string or array_nesting > 1 then # don't care if inside string or
170
+ sbuffer += char # nested array
171
+ else
172
+ if !sbuffer.is_a? ::Array then
173
+ sbuffer = @base_type.parse(sbuffer)
174
+ end
175
+ result_array << sbuffer # otherwise, here ends an element
176
+ sbuffer = ''
177
+ end
178
+
179
+ #------------- }: End of Array --------------#
180
+ when '}'
181
+ if in_string then # ignore if inside quoted string
182
+ sbuffer += char
183
+ next
184
+ end
185
+
186
+ array_nesting -=1 # decrease nesting level
187
+
188
+ if array_nesting == 1 # must be the end of a nested array
189
+ sbuffer += char
190
+ sbuffer = convert_array( sbuffer ) # recurse, using the whole nested array
191
+ elsif array_nesting > 1 # inside nested array, keep it for later
192
+ sbuffer += char
193
+ else # array_nesting = 0, must be the last }
194
+ if !sbuffer.is_a? ::Array then
195
+ sbuffer = @base_type.parse( sbuffer )
196
+ end
197
+
198
+ result_array << sbuffer unless sbuffer.nil? # upto here was the last element
199
+ end
200
+
201
+ #------------- all other characters ---------#
202
+ else
203
+ sbuffer += char # simply append
204
+ end
205
+ }
206
+ return result_array
207
+ end # convert_array()
208
+ end
209
+ end
@@ -0,0 +1,151 @@
1
+ #
2
+ # See DBI::BaseDatabase.
3
+ #
4
+ class DBI::DBD::SQLite::Database < DBI::BaseDatabase
5
+ attr_reader :db
6
+ attr_reader :attr_hash
7
+ attr_accessor :open_handles
8
+
9
+ #
10
+ # Constructor. Valid attributes:
11
+ #
12
+ # * AutoCommit: Commit after every statement execution.
13
+ #
14
+ def initialize(dbname, user, auth, attr_hash)
15
+ # FIXME why isn't this crap being done in DBI?
16
+ unless dbname.kind_of? String
17
+ raise DBI::InterfaceError, "Database Name must be a string"
18
+ end
19
+
20
+ unless dbname.length > 0
21
+ raise DBI::InterfaceError, "Database Name needs to be length > 0"
22
+ end
23
+
24
+ unless attr_hash.kind_of? Hash
25
+ raise DBI::InterfaceError, "Attributes should be a hash"
26
+ end
27
+
28
+ # FIXME handle busy_timeout in SQLite driver
29
+ # FIXME handle SQLite pragmas in SQLite driver
30
+ @attr_hash = attr_hash
31
+ @open_handles = 0
32
+
33
+ self["AutoCommit"] = true if self["AutoCommit"].nil?
34
+
35
+ # open the database
36
+ begin
37
+ @db = ::SQLite::Database.new(dbname)
38
+ rescue Exception => e
39
+ raise DBI::OperationalError, "Couldn't open database #{dbname}: #{e.message}"
40
+ end
41
+ end
42
+
43
+ def disconnect
44
+ rollback rescue nil
45
+ @db.close if @db and !@db.closed?
46
+ @db = nil
47
+ end
48
+
49
+ def database_name
50
+ st = DBI::DBD::SQLite::Statement.new('PRAGMA database_list', self)
51
+ st.execute
52
+ row = st.fetch
53
+ st.finish
54
+
55
+ return row[2]
56
+ end
57
+
58
+ def prepare(stmt)
59
+ return DBI::DBD::SQLite::Statement.new(stmt, self)
60
+ end
61
+
62
+ def ping
63
+ return !@db.closed?
64
+ end
65
+
66
+ def tables
67
+ sth = prepare("select name from sqlite_master where type in ('table', 'view')")
68
+ sth.execute
69
+ tables = sth.fetch_all.flatten
70
+ sth.finish
71
+ return tables
72
+ # FIXME does sqlite use views too? not sure, but they need to be included according to spec
73
+ end
74
+
75
+ def commit
76
+ @db.commit if @db.transaction_active?
77
+ end
78
+
79
+ #
80
+ # Rollback the transaction. SQLite has some issues with open statement
81
+ # handles when this happens. If there are still open handles, a
82
+ # DBI::Warning exception will be raised.
83
+ #
84
+ def rollback
85
+ if @open_handles > 0
86
+ raise DBI::Warning, "Leaving unfinished select statement handles while rolling back a transaction can corrupt your database or crash your program"
87
+ end
88
+
89
+ @db.rollback if @db.transaction_active?
90
+ end
91
+
92
+ def [](key)
93
+ return @attr_hash[key]
94
+ end
95
+
96
+ #
97
+ # See DBI::BaseDatabase#[]=.
98
+ #
99
+ # If AutoCommit is set to +true+ using this method, was previously +false+,
100
+ # and we are currently in a transaction, The act of setting this will cause
101
+ # an immediate commit.
102
+ #
103
+ def []=(key, value)
104
+
105
+ old_value = @attr_hash[key]
106
+
107
+ @attr_hash[key] = value
108
+
109
+ # special handling of settings
110
+ case key
111
+ when "AutoCommit"
112
+ # if the value being set is true and the previous value is false,
113
+ # commit the current transaction (if any)
114
+ # FIXME I still think this is a horrible way of handling this.
115
+ if value and !old_value
116
+ begin
117
+ @dbh.commit
118
+ rescue Exception => e
119
+ end
120
+ end
121
+ end
122
+
123
+ return @attr_hash[key]
124
+ end
125
+
126
+ def columns(tablename)
127
+ return nil unless tablename and tablename.kind_of? String
128
+
129
+ sth = prepare("PRAGMA table_info(?)")
130
+ sth.bind_param(1, tablename)
131
+ sth.execute
132
+ columns = [ ]
133
+ while row = sth.fetch
134
+ column = { }
135
+ column["name"] = row[1]
136
+
137
+ m = DBI::DBD::SQLite.parse_type(row[2])
138
+ column["type_name"] = m[1]
139
+ column["precision"] = m[3].to_i if m[3]
140
+ column["scale"] = m[5].to_i if m[5]
141
+
142
+ column["nullable"] = row[3].to_i == 0
143
+ column["default"] = row[4]
144
+ columns.push column
145
+ end
146
+
147
+ sth.finish
148
+ return columns
149
+ # XXX it'd be nice if the spec was changed to do this k/v with the name as the key.
150
+ end
151
+ end
@@ -0,0 +1,125 @@
1
+ #
2
+ # See DBI::BaseStatement.
3
+ #
4
+ class DBI::DBD::SQLite::Statement < DBI::BaseStatement
5
+ DBI_TYPE_MAP = [
6
+ [ /^INT(EGER)?$/i, DBI::SQL_INTEGER ],
7
+ [ /^(OID|ROWID|_ROWID_)$/i, DBI::SQL_OTHER ],
8
+ [ /^FLOAT$/i, DBI::SQL_FLOAT ],
9
+ [ /^REAL$/i, DBI::SQL_REAL ],
10
+ [ /^DOUBLE$/i, DBI::SQL_DOUBLE ],
11
+ [ /^DECIMAL/i, DBI::SQL_DECIMAL ],
12
+ [ /^(BOOL|BOOLEAN)$/i, DBI::SQL_BOOLEAN ],
13
+ [ /^TIME$/i, DBI::SQL_TIME ],
14
+ [ /^DATE$/i, DBI::SQL_DATE ],
15
+ [ /^TIMESTAMP$/i, DBI::SQL_TIMESTAMP ],
16
+ [ /^(VARCHAR|TEXT)/i, DBI::SQL_VARCHAR ],
17
+ [ /^CHAR$/i, DBI::SQL_CHAR ],
18
+ ]
19
+
20
+ def initialize(stmt, dbh)
21
+ @dbh = dbh
22
+ @statement = DBI::SQL::PreparedStatement.new(@dbh, stmt)
23
+ @attr = { }
24
+ @params = [ ]
25
+ @rows = [ ]
26
+ @result_set = nil
27
+ @dbh.open_handles += 1
28
+ end
29
+
30
+ #
31
+ # See DBI::BaseStatement#bind_param. This method will also raise
32
+ # DBI::InterfaceError if +param+ is not a Fixnum, to prevent incorrect
33
+ # binding.
34
+ #
35
+ def bind_param(param, value, attributes=nil)
36
+ unless param.kind_of? Fixnum
37
+ raise DBI::InterfaceError, "Only numeric parameters are supported"
38
+ end
39
+
40
+ @params[param-1] = value
41
+
42
+ # FIXME what to do with attributes? are they important in SQLite?
43
+ end
44
+
45
+ #
46
+ # See DBI::BaseStatement#execute.
47
+ #
48
+ # In the event AutoCommit is off and no transaction is currently executing,
49
+ # one will be opened at this point. It is your responsibility to #finish,
50
+ # #cancel, #rollback, or #commit.
51
+ #
52
+ def execute
53
+ sql = @statement.bind(@params)
54
+ # XXX sqlite re-escapes this for us automatically, it's causing trouble with everything else.
55
+ # this will probably break in a horrible manner and I will be forced to "fix" it again.
56
+ sql.gsub!(/\\\\/) { '\\' }
57
+ DBI::DBD::SQLite.check_sql(sql)
58
+
59
+ begin
60
+ unless @dbh.db.transaction_active?
61
+ @dbh.db.transaction
62
+ end
63
+ @result_set = @dbh.db.query(sql)
64
+ @dbh.commit if @dbh["AutoCommit"]
65
+ rescue Exception => e
66
+ raise DBI::DatabaseError, e.message
67
+ end
68
+ end
69
+
70
+ alias :finish :cancel
71
+
72
+ def finish
73
+ # nil out the result set
74
+ @result_set.close if @result_set
75
+ @result_set = nil
76
+ @rows = nil
77
+ @dbh.open_handles -= 1
78
+ end
79
+
80
+ def fetch
81
+ return nil if @result_set.eof?
82
+
83
+ row = @result_set.next
84
+ return nil unless row
85
+
86
+ return row
87
+ end
88
+
89
+ def column_info
90
+ columns = [ ]
91
+
92
+ # FIXME this shit should *really* be abstracted into DBI
93
+ # FIXME this still doesn't handle nullable/unique/default stuff.
94
+ @result_set.columns.each_with_index do |name, i|
95
+ columns[i] = { } unless columns[i]
96
+ columns[i]["name"] = name
97
+ type_name = @result_set.types[i]
98
+
99
+ if type_name
100
+ m = DBI::DBD::SQLite.parse_type(type_name)
101
+
102
+ columns[i]["type_name"] = m[1]
103
+ columns[i]["precision"] = m[3].to_i if m[3]
104
+ columns[i]["scale"] = m[5].to_i if m[5]
105
+ DBI_TYPE_MAP.each do |map|
106
+ if columns[i]["type_name"] =~ map[0]
107
+ columns[i]["sql_type"] = map[1]
108
+ break
109
+ end
110
+ end
111
+
112
+ case columns[i]["type_name"]
113
+ when 'double'
114
+ columns[i]["dbi_type"] = DBI::Type::Float
115
+ end
116
+ end
117
+ end
118
+
119
+ return columns
120
+ end
121
+
122
+ def rows
123
+ return @dbh.db.changes
124
+ end
125
+ end