ydbi 0.5.9 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (85) 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 +9 -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/setup.rb +1585 -0
  43. data/test/DBD_TESTS +50 -0
  44. data/test/TESTING +16 -0
  45. data/test/dbd/general/test_database.rb +206 -0
  46. data/test/dbd/general/test_statement.rb +325 -0
  47. data/test/dbd/general/test_types.rb +295 -0
  48. data/test/dbd/mysql/base.rb +26 -0
  49. data/test/dbd/mysql/down.sql +19 -0
  50. data/test/dbd/mysql/test_blob.rb +18 -0
  51. data/test/dbd/mysql/test_new_methods.rb +7 -0
  52. data/test/dbd/mysql/test_patches.rb +111 -0
  53. data/test/dbd/mysql/up.sql +28 -0
  54. data/test/dbd/odbc/base.rb +30 -0
  55. data/test/dbd/odbc/down.sql +19 -0
  56. data/test/dbd/odbc/test_new_methods.rb +12 -0
  57. data/test/dbd/odbc/test_ping.rb +10 -0
  58. data/test/dbd/odbc/test_statement.rb +44 -0
  59. data/test/dbd/odbc/test_transactions.rb +58 -0
  60. data/test/dbd/odbc/up.sql +33 -0
  61. data/test/dbd/postgresql/base.rb +31 -0
  62. data/test/dbd/postgresql/down.sql +31 -0
  63. data/test/dbd/postgresql/test_arrays.rb +179 -0
  64. data/test/dbd/postgresql/test_async.rb +121 -0
  65. data/test/dbd/postgresql/test_blob.rb +37 -0
  66. data/test/dbd/postgresql/test_bytea.rb +88 -0
  67. data/test/dbd/postgresql/test_ping.rb +10 -0
  68. data/test/dbd/postgresql/test_timestamp.rb +77 -0
  69. data/test/dbd/postgresql/test_transactions.rb +58 -0
  70. data/test/dbd/postgresql/testdbipg.rb +307 -0
  71. data/test/dbd/postgresql/up.sql +60 -0
  72. data/test/dbd/sqlite/base.rb +32 -0
  73. data/test/dbd/sqlite/test_database.rb +30 -0
  74. data/test/dbd/sqlite/test_driver.rb +68 -0
  75. data/test/dbd/sqlite/test_statement.rb +112 -0
  76. data/test/dbd/sqlite/up.sql +25 -0
  77. data/test/dbd/sqlite3/base.rb +32 -0
  78. data/test/dbd/sqlite3/test_database.rb +77 -0
  79. data/test/dbd/sqlite3/test_driver.rb +67 -0
  80. data/test/dbd/sqlite3/test_statement.rb +88 -0
  81. data/test/dbd/sqlite3/up.sql +33 -0
  82. data/test/dbi/tc_dbi.rb +1 -1
  83. data/test/ts_dbd.rb +136 -0
  84. data/ydbi.gemspec +25 -0
  85. metadata +150 -13
@@ -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