Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb

Revision 8969, 17.1 kB (checked in by nzkoz, 3 months ago)

Make the mysql schema dumper roundtrip the limits of text/blob columns. Closes #7424 [will.bryant]

  • Property svn:executable set to *
Line 
1 require 'active_record/connection_adapters/abstract_adapter'
2 require 'set'
3
4 module MysqlCompat #:nodoc:
5   # add all_hashes method to standard mysql-c bindings or pure ruby version
6   def self.define_all_hashes_method!
7     raise 'Mysql not loaded' unless defined?(::Mysql)
8
9     target = defined?(Mysql::Result) ? Mysql::Result : MysqlRes
10     return if target.instance_methods.include?('all_hashes')
11
12     # Ruby driver has a version string and returns null values in each_hash
13     # C driver >= 2.7 returns null values in each_hash
14     if Mysql.const_defined?(:VERSION) && (Mysql::VERSION.is_a?(String) || Mysql::VERSION >= 20700)
15       target.class_eval <<-'end_eval'
16       def all_hashes
17         rows = []
18         each_hash { |row| rows << row }
19         rows
20       end
21       end_eval
22
23     # adapters before 2.7 don't have a version constant
24     # and don't return null values in each_hash
25     else
26       target.class_eval <<-'end_eval'
27       def all_hashes
28         rows = []
29         all_fields = fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields }
30         each_hash { |row| rows << all_fields.dup.update(row) }
31         rows
32       end
33       end_eval
34     end
35
36     unless target.instance_methods.include?('all_hashes') ||
37            target.instance_methods.include?(:all_hashes)
38       raise "Failed to defined #{target.name}#all_hashes method. Mysql::VERSION = #{Mysql::VERSION.inspect}"
39     end
40   end
41 end
42
43 module ActiveRecord
44   class Base
45     def self.require_mysql
46       # Include the MySQL driver if one hasn't already been loaded
47       unless defined? Mysql
48         begin
49           require_library_or_gem 'mysql'
50         rescue LoadError => cannot_require_mysql
51           # Use the bundled Ruby/MySQL driver if no driver is already in place
52           begin
53             ActiveRecord::Base.logger.info(
54               "WARNING: You're using the Ruby-based MySQL library that ships with Rails. This library is not suited for production. " +
55               "Please install the C-based MySQL library instead (gem install mysql)."
56             ) if ActiveRecord::Base.logger
57
58             require 'active_record/vendor/mysql'
59           rescue LoadError
60             raise cannot_require_mysql
61           end
62         end
63       end
64
65       # Define Mysql::Result.all_hashes
66       MysqlCompat.define_all_hashes_method!
67     end
68
69     # Establishes a connection to the database that's used by all Active Record objects.
70     def self.mysql_connection(config) # :nodoc:
71       config = config.symbolize_keys
72       host     = config[:host]
73       port     = config[:port]
74       socket   = config[:socket]
75       username = config[:username] ? config[:username].to_s : 'root'
76       password = config[:password].to_s
77
78       if config.has_key?(:database)
79         database = config[:database]
80       else
81         raise ArgumentError, "No database specified. Missing argument: database."
82       end
83
84       require_mysql
85       mysql = Mysql.init
86       mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]
87
88       ConnectionAdapters::MysqlAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
89     end
90   end
91
92   module ConnectionAdapters
93     class MysqlColumn < Column #:nodoc:
94       def extract_default(default)
95         if type == :binary || type == :text
96           if default.blank?
97             nil
98           else
99             raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
100           end
101         elsif missing_default_forged_as_empty_string?(default)
102           nil
103         else
104           super
105         end
106       end
107
108       private
109         def simplified_type(field_type)
110           return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
111           return :string  if field_type =~ /enum/i
112           super
113         end
114
115         def extract_limit(sql_type)
116           if sql_type =~ /blob|text/i
117             case sql_type
118             when /tiny/i
119               255
120             when /medium/i
121               16777215
122             when /long/i
123               2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
124             else
125               super # we could return 65535 here, but we leave it undecorated by default
126             end
127           else
128             super
129           end
130         end
131
132         # MySQL misreports NOT NULL column default when none is given.
133         # We can't detect this for columns which may have a legitimate ''
134         # default (string) but we can for others (integer, datetime, boolean,
135         # and the rest).
136         #
137         # Test whether the column has default '', is not null, and is not
138         # a type allowing default ''.
139         def missing_default_forged_as_empty_string?(default)
140           type != :string && !null && default == ''
141         end
142     end
143
144     # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
145     # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/).
146     #
147     # Options:
148     #
149     # * <tt>:host</tt> -- Defaults to localhost
150     # * <tt>:port</tt> -- Defaults to 3306
151     # * <tt>:socket</tt> -- Defaults to /tmp/mysql.sock
152     # * <tt>:username</tt> -- Defaults to root
153     # * <tt>:password</tt> -- Defaults to nothing
154     # * <tt>:database</tt> -- The name of the database. No default, must be provided.
155     # * <tt>:encoding</tt> -- (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection
156     # * <tt>:sslkey</tt> -- Necessary to use MySQL with an SSL connection
157     # * <tt>:sslcert</tt> -- Necessary to use MySQL with an SSL connection
158     # * <tt>:sslcapath</tt> -- Necessary to use MySQL with an SSL connection
159     # * <tt>:sslcipher</tt> -- Necessary to use MySQL with an SSL connection
160     #
161     # By default, the MysqlAdapter will consider all columns of type tinyint(1)
162     # as boolean. If you wish to disable this emulation (which was the default
163     # behavior in versions 0.13.1 and earlier) you can add the following line
164     # to your environment.rb file:
165     #
166     #   ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
167     class MysqlAdapter < AbstractAdapter
168       @@emulate_booleans = true
169       cattr_accessor :emulate_booleans
170
171       LOST_CONNECTION_ERROR_MESSAGES = [
172         "Server shutdown in progress",
173         "Broken pipe",
174         "Lost connection to MySQL server during query",
175         "MySQL server has gone away" ]
176
177       QUOTED_TRUE, QUOTED_FALSE = '1', '0'
178
179       def initialize(connection, logger, connection_options, config)
180         super(connection, logger)
181         @connection_options, @config = connection_options, config
182         @quoted_column_names, @quoted_table_names = {}, {}
183         connect
184       end
185
186       def adapter_name #:nodoc:
187         'MySQL'
188       end
189
190       def supports_migrations? #:nodoc:
191         true
192       end
193
194       def native_database_types #:nodoc:
195         {
196           :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
197           :string      => { :name => "varchar", :limit => 255 },
198           :text        => { :name => "text" },
199           :integer     => { :name => "int", :limit => 11 },
200           :float       => { :name => "float" },
201           :decimal     => { :name => "decimal" },
202           :datetime    => { :name => "datetime" },
203           :timestamp   => { :name => "datetime" },
204           :time        => { :name => "time" },
205           :date        => { :name => "date" },
206           :binary      => { :name => "blob" },
207           :boolean     => { :name => "tinyint", :limit => 1 }
208         }
209       end
210
211
212       # QUOTING ==================================================
213
214       def quote(value, column = nil)
215         if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
216           s = column.class.string_to_binary(value).unpack("H*")[0]
217           "x'#{s}'"
218         elsif value.kind_of?(BigDecimal)
219           "'#{value.to_s("F")}'"
220         else
221           super
222         end
223       end
224
225       def quote_column_name(name) #:nodoc:
226         @quoted_column_names[name] ||= "`#{name}`"
227       end
228
229       def quote_table_name(name) #:nodoc:
230         @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
231       end
232
233       def quote_string(string) #:nodoc:
234         @connection.quote(string)
235       end
236
237       def quoted_true
238         QUOTED_TRUE
239       end
240
241       def quoted_false
242         QUOTED_FALSE
243       end
244
245       # REFERENTIAL INTEGRITY ====================================
246
247       def disable_referential_integrity(&block) #:nodoc:
248         old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
249
250         begin
251           update("SET FOREIGN_KEY_CHECKS = 0")
252           yield
253         ensure
254           update("SET FOREIGN_KEY_CHECKS = #{old}")
255         end
256       end
257
258       # CONNECTION MANAGEMENT ====================================
259
260       def active?
261         if @connection.respond_to?(:stat)
262           @connection.stat
263         else
264           @connection.query 'select 1'
265         end
266
267         # mysql-ruby doesn't raise an exception when stat fails.
268         if @connection.respond_to?(:errno)
269           @connection.errno.zero?
270         else
271           true
272         end
273       rescue Mysql::Error
274         false
275       end
276
277       def reconnect!
278         disconnect!
279         connect
280       end
281
282       def disconnect!
283         @connection.close rescue nil
284       end
285
286
287       # DATABASE STATEMENTS ======================================
288
289       def select_rows(sql, name = nil)
290         @connection.query_with_result = true
291         result = execute(sql, name)
292         rows = []
293         result.each { |row| rows << row }
294         result.free
295         rows
296       end
297
298       def execute(sql, name = nil) #:nodoc:
299         log(sql, name) { @connection.query(sql) }
300       rescue ActiveRecord::StatementInvalid => exception
301         if exception.message.split(":").first =~ /Packets out of order/
302           raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information.  If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
303         else
304           raise
305         end
306       end
307
308       def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
309         super sql, name
310         id_value || @connection.insert_id
311       end
312
313       def update_sql(sql, name = nil) #:nodoc:
314         super
315         @connection.affected_rows
316       end
317
318       def begin_db_transaction #:nodoc:
319         execute "BEGIN"
320       rescue Exception
321         # Transactions aren't supported
322       end
323
324       def commit_db_transaction #:nodoc:
325         execute "COMMIT"
326       rescue Exception
327         # Transactions aren't supported
328       end
329
330       def rollback_db_transaction #:nodoc:
331         execute "ROLLBACK"
332       rescue Exception
333         # Transactions aren't supported
334       end
335
336
337       def add_limit_offset!(sql, options) #:nodoc:
338         if limit = options[:limit]
339           unless offset = options[:offset]
340             sql << " LIMIT #{limit}"
341           else
342             sql << " LIMIT #{offset}, #{limit}"
343           end
344         end
345       end
346
347
348       # SCHEMA STATEMENTS ========================================
349
350       def structure_dump #:nodoc:
351         if supports_views?
352           sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
353         else
354           sql = "SHOW TABLES"
355         end
356
357         select_all(sql).inject("") do |structure, table|
358           table.delete('Table_type')
359           structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n"
360         end
361       end
362
363       def recreate_database(name) #:nodoc:
364         drop_database(name)
365         create_database(name)
366       end
367
368       # Create a new MySQL database with optional :charset and :collation.
369       # Charset defaults to utf8.
370       #
371       # Example:
372       #   create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
373       #   create_database 'matt_development'
374       #   create_database 'matt_development', :charset => :big5
375       def create_database(name, options = {})
376         if options[:collation]
377           execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
378         else
379           execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
380         end
381       end
382
383       def drop_database(name) #:nodoc:
384         execute "DROP DATABASE IF EXISTS `#{name}`"
385       end
386
387       def current_database
388         select_value 'SELECT DATABASE() as db'
389       end
390
391       # Returns the database character set.
392       def charset
393         show_variable 'character_set_database'
394       end
395
396       # Returns the database collation strategy.
397       def collation
398         show_variable 'collation_database'
399       end
400
401       def tables(name = nil) #:nodoc:
402         tables = []
403         execute("SHOW TABLES", name).each { |field| tables << field[0] }
404         tables
405       end
406
407       def drop_table(table_name, options = {})
408         super(table_name, options)
409       end
410
411       def indexes(table_name, name = nil)#:nodoc:
412         indexes = []
413         current_index = nil
414         execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name).each do |row|
415           if current_index != row[2]
416             next if row[2] == "PRIMARY" # skip the primary key
417             current_index = row[2]
418             indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [])
419           end
420
421           indexes.last.columns << row[4]
422         end
423         indexes
424       end
425
426       def columns(table_name, name = nil)#:nodoc:
427         sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
428         columns = []
429         execute(sql, name).each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
430         columns
431       end
432
433       def create_table(table_name, options = {}) #:nodoc:
434         super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
435       end
436
437       def rename_table(table_name, new_name)
438         execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
439       end
440
441       def change_column_default(table_name, column_name, default) #:nodoc:
442         current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
443
444         execute("ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{current_type} DEFAULT #{quote(default)}")
445       end
446
447       def change_column(table_name, column_name, type, options = {}) #:nodoc:
448         unless options_include_default?(options)
449           if column = columns(table_name).find { |c| c.name == column_name.to_s }
450             options[:default] = column.default
451           else
452             raise "No such column: #{table_name}.#{column_name}"
453           end
454         end
455
456         change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
457         add_column_options!(change_column_sql, options)
458         execute(change_column_sql)
459       end
460
461       def rename_column(table_name, column_name, new_column_name) #:nodoc:
462         current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
463         execute "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
464       end
465
466
467       # SHOW VARIABLES LIKE 'name'
468       def show_variable(name)
469         variables = select_all("SHOW VARIABLES LIKE '#{name}'")
470         variables.first['Value'] unless variables.empty?
471       end
472
473       # Returns a table's primary key and belonging sequence.
474       def pk_and_sequence_for(table) #:nodoc:
475         keys = []
476         execute("describe #{quote_table_name(table)}").each_hash do |h|
477           keys << h["Field"]if h["Key"] == "PRI"
478         end
479         keys.length == 1 ? [keys.first, nil] : nil
480       end
481
482       private
483         def connect
484           encoding = @config[:encoding]
485           if encoding
486             @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
487           end
488           @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
489           @connection.real_connect(*@connection_options)
490           execute("SET NAMES '#{encoding}'") if encoding
491
492           # By default, MySQL 'where id is null' selects the last inserted id.
493           # Turn this off. http://dev.rubyonrails.org/ticket/6778
494           execute("SET SQL_AUTO_IS_NULL=0")
495         end
496
497         def select(sql, name = nil)
498           @connection.query_with_result = true
499           result = execute(sql, name)
500           rows = result.all_hashes
501           result.free
502           rows
503         end
504
505         def supports_views?
506           version[0] >= 5
507         end
508
509         def version
510           @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
511         end
512     end
513   end
514 end
Note: See TracBrowser for help on using the browser.