rails-dbd-mysql 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.
@@ -0,0 +1,125 @@
1
+ module DBI::DBD::Mysql
2
+ #
3
+ # Models the DBI::BaseDriver API to create DBI::DriverHandle objects.
4
+ #
5
+ class Driver < DBI::BaseDriver
6
+ include Util
7
+
8
+ def initialize
9
+ super("0.4.0")
10
+ end
11
+
12
+ def default_user
13
+ ['', nil]
14
+ end
15
+
16
+ #
17
+ # Parameters in the dbname as follows:
18
+ #
19
+ # * host: host to connect to
20
+ # * port: port to connect to
21
+ # * socket: connect to a specific unix socket instead of a TCP socket.
22
+ # * flag: an OR'd collection of flags to pass to the lower-level
23
+ # connection attempt.
24
+ # * mysql_read_default_file: FIXME
25
+ # * mysql_read_default_group: FIXME
26
+ # * mysql_compression: FIXME
27
+ # * mysql_local_infile: FIXME
28
+ # * mysql_client_found_rows: FIXME boolean, modifies the 'flag'
29
+ # setting above.
30
+ def connect(dbname, user, auth, attr)
31
+ # connect to database server
32
+ hash = DBI::Utils.parse_params(dbname)
33
+
34
+ hash['host'] ||= 'localhost'
35
+
36
+ # these two connection parameters should be passed as numbers
37
+ hash['port'] = hash['port'].to_i unless hash['port'].nil?
38
+ hash['flag'] = hash['flag'].nil? ? 0 : hash['flag'] = hash['flag'].to_i
39
+
40
+ handle = ::Mysql.init
41
+
42
+ # Look for options in connect string to be handled
43
+ # through mysql_options() before connecting
44
+ !hash['mysql_read_default_file'].nil? and
45
+ handle.options(::Mysql::READ_DEFAULT_FILE,
46
+ hash['mysql_read_default_file'])
47
+ !hash['mysql_read_default_group'].nil? and
48
+ handle.options(::Mysql::READ_DEFAULT_GROUP,
49
+ hash['mysql_read_default_group'])
50
+ # The following options can be handled either using mysql_options()
51
+ # or in the flag argument to connect().
52
+ hash['mysql_compression'].to_i != 0 and
53
+ handle.options(::Mysql::OPT_COMPRESS, nil)
54
+ hash['mysql_local_infile'].to_i != 0 and
55
+ handle.options(::Mysql::OPT_LOCAL_INFILE, true)
56
+
57
+ # Look for options to be handled in the flags argument to connect()
58
+ if !hash['mysql_client_found_rows'].nil?
59
+ if hash['mysql_client_found_rows'].to_i != 0
60
+ hash['flag'] |= ::Mysql::CLIENT_FOUND_ROWS
61
+ else
62
+ hash['flag'] &= ~::Mysql::CLIENT_FOUND_ROWS
63
+ end
64
+ end
65
+
66
+ handle.connect(hash['host'], user, auth, hash['database'], hash['port'], hash['socket'], hash['flag'])
67
+
68
+ return Database.new(handle, attr)
69
+ rescue MyError => err
70
+ error(err)
71
+ end
72
+
73
+ def data_sources
74
+ handle = ::Mysql.new
75
+ res = handle.list_dbs.collect {|db| "dbi:Mysql:database=#{db}" }
76
+ handle.close
77
+ return res
78
+ rescue MyError => err
79
+ error(err)
80
+ end
81
+
82
+ #--
83
+ # Driver-specific functions ------------------------------------------------
84
+ #++
85
+
86
+ public
87
+
88
+ def __createdb(db, host, user, password, port=nil, sock=nil, flag=nil)
89
+ handle = ::Mysql.connect(host, user, password, nil, port, sock, flag)
90
+ begin
91
+ handle.create_db(db)
92
+ ensure
93
+ handle.close if handle
94
+ end
95
+ end
96
+
97
+ def __dropdb(db, host, user, password, port=nil, sock=nil, flag=nil)
98
+ handle = ::Mysql.connect(host, user, password, nil, port, sock, flag)
99
+ begin
100
+ handle.drop_db(db)
101
+ ensure
102
+ handle.close if handle
103
+ end
104
+ end
105
+
106
+ def __shutdown(host, user, password, port=nil, sock=nil, flag=nil)
107
+ handle = ::Mysql.connect(host, user, password, nil, port, sock, flag)
108
+ begin
109
+ handle.shutdown
110
+ ensure
111
+ handle.close if handle
112
+ end
113
+ end
114
+
115
+ def __reload(host, user, password, port=nil, sock=nil, flag=nil)
116
+ handle = ::Mysql.connect(host, user, password, nil, port, sock, flag)
117
+ begin
118
+ handle.reload
119
+ ensure
120
+ handle.close if handle
121
+ end
122
+ end
123
+
124
+ end # class Driver
125
+ end
@@ -0,0 +1,188 @@
1
+ module DBI::DBD::Mysql
2
+ #
3
+ # Models the DBI::BaseStatement API to create DBI::StatementHandle objects.
4
+ #
5
+ class Statement < DBI::BaseStatement
6
+ include Util
7
+
8
+ def initialize(parent, handle, statement, mutex)
9
+ super(nil)
10
+
11
+ @parent, @handle, @mutex = parent, handle, mutex
12
+ @params = []
13
+
14
+ @prep_stmt = DBI::SQL::PreparedStatement.new(@parent, statement)
15
+ end
16
+
17
+ #
18
+ # See DBI::BaseStatement#bind_param. This method will also raise
19
+ # DBI::InterfaceError if +param+ is not a Fixnum, to prevent incorrect
20
+ # binding.
21
+ #
22
+ def bind_param(param, value, attribs)
23
+ raise InterfaceError, "only ? parameters supported" unless param.is_a? Fixnum
24
+ @params[param-1] = value
25
+ end
26
+
27
+ #
28
+ # See DBI::BaseStatement#execute. If DBI thinks this is a query via DBI::SQL.query?(),
29
+ # it will force the row processed count to 0. Otherwise, it will return
30
+ # what MySQL thinks is the row processed count.
31
+ #
32
+ def execute
33
+ sql = @prep_stmt.bind(@params)
34
+ @mutex.synchronize {
35
+ @handle.query_with_result = true
36
+ @res_handle = @handle.query(sql)
37
+ @column_info = self.column_info
38
+ @current_row = 0
39
+ @rows = DBI::SQL.query?(sql) ? 0 : @handle.affected_rows
40
+ }
41
+ rescue MyError => err
42
+ error(err)
43
+ end
44
+
45
+ def finish
46
+ @res_handle.free if @res_handle
47
+ rescue MyError => err
48
+ error(err)
49
+ end
50
+
51
+ #
52
+ # Helper method to aid #fetch. Do not call directly.
53
+ #
54
+ def fill_array(rowdata)
55
+ return nil if rowdata.nil?
56
+ return rowdata.dup
57
+ end
58
+
59
+ def fetch
60
+ @current_row += 1
61
+ fill_array(@res_handle.fetch_row)
62
+ rescue MyError => err
63
+ error(err)
64
+ end
65
+
66
+ #
67
+ # See DBI::BaseStatement#fetch_scroll. These additional constants are also supported:
68
+ #
69
+ # * DBI::SQL_FETCH_PRIOR: Fetch the row previous to the current one.
70
+ # * DBI::SQL_FETCH_FIRST: Fetch the first row.
71
+ # * DBI::SQL_FETCH_ABSOLUTE: Fetch the row at the offset provided.
72
+ # * DBI::SQL_FETCH_RELATIVE: Fetch the row at the current point + offset.
73
+ #
74
+ def fetch_scroll(direction, offset)
75
+ case direction
76
+ when DBI::SQL_FETCH_NEXT
77
+ @current_row += 1
78
+ fill_array(@res_handle.fetch_row)
79
+ when DBI::SQL_FETCH_PRIOR
80
+ @res_handle.data_seek(@current_row - 1)
81
+ fill_array(@res_handle.fetch_row)
82
+ when DBI::SQL_FETCH_FIRST
83
+ @current_row = 1
84
+ @res_handle.data_seek(@current_row - 1)
85
+ fill_array(@res_handle.fetch_row)
86
+ when DBI::SQL_FETCH_LAST
87
+ @current_row = @res_handle.num_rows
88
+ @res_handle.data_seek(@current_row - 1)
89
+ fill_array(@res_handle.fetch_row)
90
+ when DBI::SQL_FETCH_ABSOLUTE
91
+ @current_row = offset + 1
92
+ @res_handle.data_seek(@current_row - 1)
93
+ fill_array(@res_handle.fetch_row)
94
+ when DBI::SQL_FETCH_RELATIVE
95
+ @current_row += offset + 1
96
+ @res_handle.data_seek(@current_row - 1)
97
+ fill_array(@res_handle.fetch_row)
98
+ else
99
+ raise NotSupportedError
100
+ end
101
+ #end
102
+ end
103
+
104
+ #
105
+ # See DBI::BaseStatement#column_info, and DBI::DBD::Mysql::Database#columns.
106
+ #
107
+ # This method provides all the attributes the +columns+ method
108
+ # provides, and a few others:
109
+ #
110
+ # * mysql_type: These correspond to constants in the Mysql::Types
111
+ # package, in the lower-level 'mysql' package.
112
+ # * mysql_type_name: A text representation of +mysql_type+.
113
+ # * mysql_length: The length of the column.
114
+ # * mysql_max_length: The max length of the column. FIXME DESCRIBE
115
+ # DIFFERENCE
116
+ # * mysql_flags: Internal MySQL flags on this column.
117
+ #
118
+ def column_info
119
+ retval = []
120
+
121
+ return [] if @res_handle.nil?
122
+
123
+ unique_key_flag = MysqlField.const_get(:UNIQUE_KEY_FLAG)
124
+ multiple_key_flag = MysqlField.const_get(:MULTIPLE_KEY_FLAG)
125
+ indexed = (unique_key_flag | multiple_key_flag)
126
+
127
+ # Note: Cannot get 'default' column attribute because MysqlField.def
128
+ # is set only by mysql_list_fields()
129
+
130
+ @res_handle.fetch_fields.each {|col|
131
+ mysql_type_name, dbi_type = Database::TYPE_MAP[col.type] rescue [nil, nil]
132
+ xopen_info = Database::MYSQL_to_XOPEN[mysql_type_name] ||
133
+ Database::MYSQL_to_XOPEN[nil]
134
+ sql_type = xopen_info[0]
135
+ type_name = DBI::SQL_TYPE_NAMES[sql_type]
136
+
137
+ retval << {
138
+ # Standard Ruby DBI column attributes
139
+ 'name' => col.name,
140
+ 'sql_type' => sql_type,
141
+ 'type_name' => type_name,
142
+ # XXX it seems mysql counts the literal decimal point when weighing in the "length".
143
+ 'precision' => type_name == "NUMERIC" ? col.length - 2 : col.length,
144
+ 'scale' => col.decimals,
145
+ 'nullable' => !col.is_not_null?,
146
+ 'indexed' => ((col.flags & indexed) != 0) ||
147
+ col.is_pri_key?,
148
+ 'primary' => col.is_pri_key?,
149
+ 'unique' => ((col.flags & unique_key_flag) != 0) ||
150
+ col.is_pri_key?,
151
+ # MySQL-specific attributes (signified by leading "mysql_")
152
+ 'mysql_type' => col.type,
153
+ 'mysql_type_name' => mysql_type_name,
154
+ 'mysql_length' => col.length,
155
+ 'mysql_max_length' => col.max_length,
156
+ 'mysql_flags' => col.flags
157
+ }
158
+
159
+ if retval[-1]['sql_type'] == DBI::SQL_TINYINT and retval[-1]['precision'] == 1
160
+ retval[-1]['dbi_type'] = DBI::Type::Boolean
161
+ elsif dbi_type
162
+ retval[-1]['dbi_type'] = dbi_type
163
+ end
164
+ }
165
+ retval
166
+ rescue MyError => err
167
+ error(err)
168
+ end
169
+
170
+ def rows
171
+ @rows
172
+ end
173
+
174
+ # def []=(attr, value)
175
+ # case attr
176
+ # when 'mysql_use_result'
177
+ # @attr['mysql_store_result'] = ! value
178
+ # @attr['mysql_use_result'] = value
179
+ # when 'mysql_store_result'
180
+ # @attr['mysql_use_result'] = ! value
181
+ # @attr['mysql_store_result'] = value
182
+ # else
183
+ # raise NotSupportedError
184
+ # end
185
+ # end
186
+
187
+ end # class Statement
188
+ end
@@ -0,0 +1,50 @@
1
+ ================================================================================
2
+ Using DBD tests
3
+ ================================================================================
4
+
5
+ Before you do anything, read the TESTING file.
6
+
7
+ Create a YAML file named .ruby-dbi.test-config.yaml in your home directory.
8
+
9
+ This file is a hash of keys that determine what you want to test and how you
10
+ access the databases related to those tests.
11
+
12
+ The key 'dbtypes' is an array which determines what tests you want to run. They
13
+ *do not* correspond to the driver names, they correspond to the test
14
+ directories that were made for them.
15
+
16
+ Each 'dbtype' has a key that contains a hash of values:
17
+ - username: the username of your account
18
+ - password: the password for your account
19
+ - dbname: the name of the database to connect to
20
+
21
+ NOTE that tests expect to connect to a database on localhost currently. This
22
+ may be fixed in the future, especially when we start writing Oracle and
23
+ SQLServer tests.
24
+
25
+ Each DBD test relies on database semantics which may not match up entirely with
26
+ this configuration. For instance, the postgresql tests expect you to be able to
27
+ work with the database directly via the `psql' client. This is something which
28
+ will eventually be remedied as time and ability allows.
29
+
30
+ Here is a sample configuration to get you started with the postgresql tests:
31
+
32
+ ################################################################################
33
+
34
+ ---
35
+ dbtypes:
36
+ - postgresql
37
+ postgresql:
38
+ username: erikh
39
+ password: monkeys
40
+ dbname: rubytest
41
+
42
+ ################################################################################
43
+
44
+ NOTE the --- is part of the file and is not a separator.
45
+
46
+ ================================================================================
47
+ Writing DBD tests
48
+ ================================================================================
49
+
50
+ Coming soon.
@@ -0,0 +1,206 @@
1
+ @class = Class.new(DBDConfig.testbase(DBDConfig.current_dbtype)) do
2
+
3
+ def test_last_statement
4
+ @sth = @dbh.prepare("select * from names")
5
+ @sth.finish
6
+ assert_equal "select * from names", @dbh.last_statement
7
+
8
+ @sth = @dbh.execute("select * from names")
9
+ @sth.finish
10
+ assert_equal "select * from names", @dbh.last_statement
11
+
12
+ @dbh.do("select * from names")
13
+ assert_equal "select * from names", @dbh.last_statement
14
+ end
15
+
16
+ def test_empty_query
17
+ ["", " ", "\t"].each do |str|
18
+ [:do, :prepare, :execute, :select_one, :select_all].each do |call|
19
+ assert_raises(DBI::InterfaceError) do
20
+ @dbh.send(call, str)
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ def test_ping
27
+ assert @dbh.ping
28
+ # XXX if it isn't obvious, this should be tested better. Not sure what
29
+ # good behavior is yet.
30
+ end
31
+
32
+ def test_columns
33
+ assert_nothing_raised do
34
+ cols = @dbh.columns("precision_test")
35
+
36
+ assert(cols)
37
+ assert_kind_of(Array, cols)
38
+ assert_equal(4, cols.length)
39
+
40
+ # the first column should always be "text_field" and have the following
41
+ # properties:
42
+ assert_equal("text_field", cols[0]["name"])
43
+ assert(!cols[0]["nullable"])
44
+
45
+ assert_equal(20, cols[0]["precision"])
46
+ # scale can be either nil or 0 for character types.
47
+ case cols[0]["scale"]
48
+ when nil
49
+ assert_equal(nil, cols[0]["scale"])
50
+ when 0
51
+ assert_equal(0, cols[0]["scale"])
52
+ else
53
+ flunk "scale can be either 0 or nil for character types"
54
+ end
55
+
56
+ assert_equal(
57
+ DBI::Type::Varchar.object_id,
58
+ DBI::TypeUtil.type_name_to_module(cols[0]["type_name"]).object_id
59
+ )
60
+
61
+ # the second column should always be "integer_field" and have the following
62
+ # properties:
63
+ assert_equal("integer_field", cols[1]["name"])
64
+ assert(cols[1]["nullable"])
65
+ assert_equal(1, cols[2]["scale"])
66
+ assert_equal(2, cols[2]["precision"])
67
+
68
+ assert_equal(
69
+ DBI::Type::Integer.object_id,
70
+ DBI::TypeUtil.type_name_to_module(cols[1]["type_name"]).object_id
71
+ )
72
+
73
+ # the second column should always be "integer_field" and have the following
74
+ # properties:
75
+ assert_equal("decimal_field", cols[2]["name"])
76
+ assert(cols[2]["nullable"])
77
+ assert_equal(1, cols[2]["scale"])
78
+ assert_equal(2, cols[2]["precision"])
79
+ assert_equal(
80
+ DBI::Type::Decimal.object_id,
81
+ DBI::TypeUtil.type_name_to_module(cols[2]["type_name"]).object_id
82
+ )
83
+
84
+ # the second column should always be "numeric_field" and have the following
85
+ # properties:
86
+ assert_equal("numeric_field", cols[3]["name"])
87
+ assert(cols[3]["nullable"])
88
+ assert_equal(6, cols[3]["scale"])
89
+ assert_equal(30, cols[3]["precision"])
90
+ assert_equal(
91
+ DBI::Type::Decimal.object_id,
92
+ DBI::TypeUtil.type_name_to_module(cols[3]["type_name"]).object_id
93
+ )
94
+
95
+ # finally, we ensure that every column in the array is a ColumnInfo
96
+ # object
97
+ cols.each { |col| assert_kind_of(DBI::ColumnInfo, col) }
98
+ end
99
+ end
100
+
101
+ def test_prepare
102
+ @sth = @dbh.prepare('select * from names')
103
+
104
+ assert @sth
105
+ assert_kind_of DBI::StatementHandle, @sth
106
+
107
+ @sth.finish
108
+ end
109
+
110
+ def test_do
111
+ assert_equal 1, @dbh.do("insert into names (name, age) values (?, ?)", "Billy", 21)
112
+ @sth = @dbh.prepare("select * from names where name = ?")
113
+ @sth.execute("Billy")
114
+ assert_equal ["Billy", 21], @sth.fetch
115
+ @sth.finish
116
+ end
117
+
118
+ def test_tables
119
+ tables = @dbh.tables.sort
120
+
121
+ # since this is a general test, let's prune the system tables
122
+ # FIXME not so sure if this should be a general test anymore.
123
+ if dbtype == "odbc"
124
+ tables -= [
125
+ "administrable_role_authorizations",
126
+ "applicable_roles",
127
+ "attributes",
128
+ "check_constraint_routine_usage",
129
+ "check_constraints",
130
+ "column_domain_usage",
131
+ "column_privileges",
132
+ "column_udt_usage",
133
+ "columns",
134
+ "constraint_column_usage",
135
+ "constraint_table_usage",
136
+ "data_type_privileges",
137
+ "domain_constraints",
138
+ "domain_udt_usage",
139
+ "domains",
140
+ "element_types",
141
+ "enabled_roles",
142
+ "information_schema_catalog_name",
143
+ "key_column_usage",
144
+ "parameters",
145
+ "referential_constraints",
146
+ "role_column_grants",
147
+ "role_routine_grants",
148
+ "role_table_grants",
149
+ "role_usage_grants",
150
+ "routine_privileges",
151
+ "routines",
152
+ "schemata",
153
+ "sequences",
154
+ "sql_features",
155
+ "sql_implementation_info",
156
+ "sql_languages",
157
+ "sql_packages",
158
+ "sql_parts",
159
+ "sql_sizing",
160
+ "sql_sizing_profiles",
161
+ "table_constraints",
162
+ "table_privileges",
163
+ "tables",
164
+ "triggered_update_columns",
165
+ "triggers",
166
+ "usage_privileges",
167
+ "view_column_usage",
168
+ "view_routine_usage",
169
+ "view_table_usage",
170
+ "views"
171
+ ]
172
+ end
173
+
174
+ case dbtype
175
+ when "postgresql"
176
+ tables.reject! { |x| x =~ /^pg_/ }
177
+ assert_equal %w(array_test bit_test blob_test boolean_test bytea_test db_specific_types_test enum_type_test field_types_test names precision_test time_test timestamp_test view_names), tables
178
+ when 'sqlite3'
179
+ assert_equal %w(bit_test blob_test boolean_test db_specific_types_test field_types_test names names_defined_with_spaces precision_test time_test timestamp_test view_names), tables
180
+ else
181
+ assert_equal %w(bit_test blob_test boolean_test db_specific_types_test field_types_test names precision_test time_test timestamp_test view_names), tables
182
+ end
183
+ end
184
+
185
+ def test_attrs
186
+ # test defaults
187
+ assert @dbh["AutoCommit"] # should be true
188
+
189
+ # test setting
190
+ assert !(@dbh["AutoCommit"] = false)
191
+ assert !@dbh["AutoCommit"]
192
+
193
+ # test committing an outstanding transaction
194
+
195
+ @sth = @dbh.prepare("insert into names (name, age) values (?, ?)")
196
+ @sth.execute("Billy", 22)
197
+ @sth.finish
198
+
199
+ assert @dbh["AutoCommit"] = true # should commit at this point
200
+
201
+ @sth = @dbh.prepare("select * from names where name = ?")
202
+ @sth.execute("Billy")
203
+ assert_equal [ "Billy", 22 ], @sth.fetch
204
+ @sth.finish
205
+ end
206
+ end