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

root/tags/rel_1-2-0/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb

Revision 5983, 13.3 kB (checked in by david, 2 years ago)

Fix nodoc breaking adapters (closes #7161)

  • 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       raise "Failed to defined #{target.name}#all_hashes method. Mysql::VERSION = #{Mysql::VERSION.inspect}"
38     end
39   end
40 end
41
42 module ActiveRecord
43   class Base
44     def self.require_mysql
45       # Include the MySQL driver if one hasn't already been loaded
46       unless defined? Mysql
47         begin
48           require_library_or_gem 'mysql'
49         rescue LoadError => cannot_require_mysql
50           # Use the bundled Ruby/MySQL driver if no driver is already in place
51           begin
52             require 'active_record/vendor/mysql'
53           rescue LoadError
54             raise cannot_require_mysql
55           end
56         end
57       end
58
59       # Define Mysql::Result.all_hashes
60       MysqlCompat.define_all_hashes_method!
61     end
62
63     # Establishes a connection to the database that's used by all Active Record objects.
64     def self.mysql_connection(config) # :nodoc:
65       config = config.symbolize_keys
66       host     = config[:host]
67       port     = config[:port]
68       socket   = config[:socket]
69       username = config[:username] ? config[:username].to_s : 'root'
70       password = config[:password].to_s
71
72       if config.has_key?(:database)
73         database = config[:database]
74       else
75         raise ArgumentError, "No database specified. Missing argument: database."
76       end
77
78       require_mysql
79       mysql = Mysql.init
80       mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]
81
82       ConnectionAdapters::MysqlAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
83     end
84   end
85
86   module ConnectionAdapters
87     class MysqlColumn < Column #:nodoc:
88       TYPES_ALLOWING_EMPTY_STRING_DEFAULT = Set.new([:binary, :string, :text])
89
90       def initialize(name, default, sql_type = nil, null = true)
91         @original_default = default
92         super
93         @default = nil if missing_default_forged_as_empty_string?
94       end
95
96       private
97         def simplified_type(field_type)
98           return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
99           return :string  if field_type =~ /enum/i
100           super
101         end
102
103         # MySQL misreports NOT NULL column default when none is given.
104         # We can't detect this for columns which may have a legitimate ''
105         # default (string, text, binary) but we can for others (integer,
106         # datetime, boolean, and the rest).
107         #
108         # Test whether the column has default '', is not null, and is not
109         # a type allowing default ''.
110         def missing_default_forged_as_empty_string?
111           !null && @original_default == '' && !TYPES_ALLOWING_EMPTY_STRING_DEFAULT.include?(type)
112         end
113     end
114
115     # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
116     # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/).
117     #
118     # Options:
119     #
120     # * <tt>:host</tt> -- Defaults to localhost
121     # * <tt>:port</tt> -- Defaults to 3306
122     # * <tt>:socket</tt> -- Defaults to /tmp/mysql.sock
123     # * <tt>:username</tt> -- Defaults to root
124     # * <tt>:password</tt> -- Defaults to nothing
125     # * <tt>:database</tt> -- The name of the database. No default, must be provided.
126     # * <tt>:sslkey</tt> -- Necessary to use MySQL with an SSL connection
127     # * <tt>:sslcert</tt> -- Necessary to use MySQL with an SSL connection
128     # * <tt>:sslcapath</tt> -- Necessary to use MySQL with an SSL connection
129     # * <tt>:sslcipher</tt> -- Necessary to use MySQL with an SSL connection
130     #
131     # By default, the MysqlAdapter will consider all columns of type tinyint(1)
132     # as boolean. If you wish to disable this emulation (which was the default
133     # behavior in versions 0.13.1 and earlier) you can add the following line
134     # to your environment.rb file:
135     #
136     #   ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
137     class MysqlAdapter < AbstractAdapter
138       @@emulate_booleans = true
139       cattr_accessor :emulate_booleans
140
141       LOST_CONNECTION_ERROR_MESSAGES = [
142         "Server shutdown in progress",
143         "Broken pipe",
144         "Lost connection to MySQL server during query",
145         "MySQL server has gone away"
146       ]
147
148       def initialize(connection, logger, connection_options, config)
149         super(connection, logger)
150         @connection_options, @config = connection_options, config
151
152         connect
153       end
154
155       def adapter_name #:nodoc:
156         'MySQL'
157       end
158
159       def supports_migrations? #:nodoc:
160         true
161       end
162
163       def native_database_types #:nodoc:
164         {
165           :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
166           :string      => { :name => "varchar", :limit => 255 },
167           :text        => { :name => "text" },
168           :integer     => { :name => "int", :limit => 11 },
169           :float       => { :name => "float" },
170           :decimal     => { :name => "decimal" },
171           :datetime    => { :name => "datetime" },
172           :timestamp   => { :name => "datetime" },
173           :time        => { :name => "time" },
174           :date        => { :name => "date" },
175           :binary      => { :name => "blob" },
176           :boolean     => { :name => "tinyint", :limit => 1 }
177         }
178       end
179
180
181       # QUOTING ==================================================
182
183       def quote(value, column = nil)
184         if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
185           s = column.class.string_to_binary(value).unpack("H*")[0]
186           "x'#{s}'"
187         elsif value.kind_of?(BigDecimal)
188           "'#{value.to_s("F")}'"
189         else
190           super
191         end
192       end
193
194       def quote_column_name(name) #:nodoc:
195         "`#{name}`"
196       end
197
198       def quote_string(string) #:nodoc:
199         @connection.quote(string)
200       end
201
202       def quoted_true
203         "1"
204       end
205      
206       def quoted_false
207         "0"
208       end
209
210
211       # CONNECTION MANAGEMENT ====================================
212
213       def active?
214         if @connection.respond_to?(:stat)
215           @connection.stat
216         else
217           @connection.query 'select 1'
218         end
219
220         # mysql-ruby doesn't raise an exception when stat fails.
221         if @connection.respond_to?(:errno)
222           @connection.errno.zero?
223         else
224           true
225         end
226       rescue Mysql::Error
227         false
228       end
229
230       def reconnect!
231         disconnect!
232         connect
233       end
234      
235       def disconnect!
236         @connection.close rescue nil
237       end
238
239
240       # DATABASE STATEMENTS ======================================
241
242       def execute(sql, name = nil) #:nodoc:
243         log(sql, name) { @connection.query(sql) }
244       rescue ActiveRecord::StatementInvalid => exception
245         if exception.message.split(":").first =~ /Packets out of order/
246           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."
247         else
248           raise
249         end
250       end
251
252       def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
253         execute(sql, name = nil)
254         id_value || @connection.insert_id
255       end
256
257       def update(sql, name = nil) #:nodoc:
258         execute(sql, name)
259         @connection.affected_rows
260       end
261
262       def begin_db_transaction #:nodoc:
263         execute "BEGIN"
264       rescue Exception
265         # Transactions aren't supported
266       end
267
268       def commit_db_transaction #:nodoc:
269         execute "COMMIT"
270       rescue Exception
271         # Transactions aren't supported
272       end
273
274       def rollback_db_transaction #:nodoc:
275         execute "ROLLBACK"
276       rescue Exception
277         # Transactions aren't supported
278       end
279
280
281       def add_limit_offset!(sql, options) #:nodoc:
282         if limit = options[:limit]
283           unless offset = options[:offset]
284             sql << " LIMIT #{limit}"
285           else
286             sql << " LIMIT #{offset}, #{limit}"
287           end
288         end
289       end
290
291
292       # SCHEMA STATEMENTS ========================================
293
294       def structure_dump #:nodoc:
295         if supports_views?
296           sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
297         else
298           sql = "SHOW TABLES"
299         end
300        
301         select_all(sql).inject("") do |structure, table|
302           table.delete('Table_type')
303           structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n"
304         end
305       end
306
307       def recreate_database(name) #:nodoc:
308         drop_database(name)
309         create_database(name)
310       end
311
312       def create_database(name) #:nodoc:
313         execute "CREATE DATABASE `#{name}`"
314       end
315      
316       def drop_database(name) #:nodoc:
317         execute "DROP DATABASE IF EXISTS `#{name}`"
318       end
319
320       def current_database
321         select_one("SELECT DATABASE() as db")["db"]
322       end
323
324       def tables(name = nil) #:nodoc:
325         tables = []
326         execute("SHOW TABLES", name).each { |field| tables << field[0] }
327         tables
328       end
329
330       def indexes(table_name, name = nil)#:nodoc:
331         indexes = []
332         current_index = nil
333         execute("SHOW KEYS FROM #{table_name}", name).each do |row|
334           if current_index != row[2]
335             next if row[2] == "PRIMARY" # skip the primary key
336             current_index = row[2]
337             indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [])
338           end
339
340           indexes.last.columns << row[4]
341         end
342         indexes
343       end
344
345       def columns(table_name, name = nil)#:nodoc:
346         sql = "SHOW FIELDS FROM #{table_name}"
347         columns = []
348         execute(sql, name).each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
349         columns
350       end
351
352       def create_table(name, options = {}) #:nodoc:
353         super(name, {:options => "ENGINE=InnoDB"}.merge(options))
354       end
355      
356       def rename_table(name, new_name)
357         execute "RENAME TABLE #{name} TO #{new_name}"
358       end 
359
360       def change_column_default(table_name, column_name, default) #:nodoc:
361         current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
362
363         execute("ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{current_type} DEFAULT #{quote(default)}")
364       end
365
366       def change_column(table_name, column_name, type, options = {}) #:nodoc:
367         unless options_include_default?(options)
368           options[:default] = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Default"]
369         end
370
371         change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
372         add_column_options!(change_column_sql, options)
373         execute(change_column_sql)
374       end
375
376       def rename_column(table_name, column_name, new_column_name) #:nodoc:
377         current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
378         execute "ALTER TABLE #{table_name} CHANGE #{column_name} #{new_column_name} #{current_type}"
379       end
380
381
382       private
383         def connect
384           encoding = @config[:encoding]
385           if encoding
386             @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
387           end
388           @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
389           @connection.real_connect(*@connection_options)
390           execute("SET NAMES '#{encoding}'") if encoding
391         end
392
393         def select(sql, name = nil)
394           @connection.query_with_result = true
395           result = execute(sql, name)
396           rows = result.all_hashes
397           result.free
398           rows
399         end
400
401         def supports_views?
402           version[0] >= 5
403         end
404
405         def version
406           @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
407         end
408     end
409   end
410 end
Note: See TracBrowser for help on using the browser.