rails-dbd-mysql 0.1.0

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