rdp-mysql2 0.2.7.1

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.
Files changed (50) hide show
  1. data/.gitignore +12 -0
  2. data/.rspec +2 -0
  3. data/.rvmrc +1 -0
  4. data/CHANGELOG.md +142 -0
  5. data/Gemfile +3 -0
  6. data/MIT-LICENSE +20 -0
  7. data/README.rdoc +261 -0
  8. data/Rakefile +5 -0
  9. data/benchmark/active_record.rb +51 -0
  10. data/benchmark/active_record_threaded.rb +42 -0
  11. data/benchmark/allocations.rb +33 -0
  12. data/benchmark/escape.rb +36 -0
  13. data/benchmark/query_with_mysql_casting.rb +80 -0
  14. data/benchmark/query_without_mysql_casting.rb +47 -0
  15. data/benchmark/sequel.rb +37 -0
  16. data/benchmark/setup_db.rb +119 -0
  17. data/benchmark/threaded.rb +44 -0
  18. data/examples/eventmachine.rb +21 -0
  19. data/examples/threaded.rb +20 -0
  20. data/ext/mysql2/client.c +839 -0
  21. data/ext/mysql2/client.h +41 -0
  22. data/ext/mysql2/extconf.rb +72 -0
  23. data/ext/mysql2/mysql2_ext.c +12 -0
  24. data/ext/mysql2/mysql2_ext.h +42 -0
  25. data/ext/mysql2/result.c +488 -0
  26. data/ext/mysql2/result.h +20 -0
  27. data/lib/active_record/connection_adapters/em_mysql2_adapter.rb +64 -0
  28. data/lib/active_record/connection_adapters/mysql2_adapter.rb +654 -0
  29. data/lib/active_record/fiber_patches.rb +104 -0
  30. data/lib/arel/engines/sql/compilers/mysql2_compiler.rb +11 -0
  31. data/lib/mysql2.rb +16 -0
  32. data/lib/mysql2/client.rb +240 -0
  33. data/lib/mysql2/em.rb +37 -0
  34. data/lib/mysql2/em_fiber.rb +31 -0
  35. data/lib/mysql2/error.rb +15 -0
  36. data/lib/mysql2/result.rb +5 -0
  37. data/lib/mysql2/version.rb +3 -0
  38. data/mysql2.gemspec +32 -0
  39. data/spec/em/em_fiber_spec.rb +22 -0
  40. data/spec/em/em_spec.rb +49 -0
  41. data/spec/mysql2/client_spec.rb +430 -0
  42. data/spec/mysql2/error_spec.rb +69 -0
  43. data/spec/mysql2/result_spec.rb +333 -0
  44. data/spec/rcov.opts +3 -0
  45. data/spec/spec_helper.rb +66 -0
  46. data/tasks/benchmarks.rake +20 -0
  47. data/tasks/compile.rake +71 -0
  48. data/tasks/rspec.rake +16 -0
  49. data/tasks/vendor_mysql.rake +40 -0
  50. metadata +236 -0
@@ -0,0 +1,20 @@
1
+ #ifndef MYSQL2_RESULT_H
2
+ #define MYSQL2_RESULT_H
3
+
4
+ void init_mysql2_result();
5
+ VALUE rb_mysql_result_to_obj(MYSQL_RES * r);
6
+
7
+ typedef struct {
8
+ VALUE fields;
9
+ VALUE rows;
10
+ VALUE encoding;
11
+ unsigned int numberOfFields;
12
+ unsigned long numberOfRows;
13
+ unsigned long lastRowProcessed;
14
+ char resultFreed;
15
+ MYSQL_RES *result;
16
+ } mysql2_result_wrapper;
17
+
18
+ #define GetMysql2Result(obj, sval) (sval = (mysql2_result_wrapper*)DATA_PTR(obj));
19
+
20
+ #endif
@@ -0,0 +1,64 @@
1
+ # encoding: utf-8
2
+
3
+ # AR adapter for using a fibered mysql2 connection with EM
4
+ # This adapter should be used within Thin or Unicorn with the rack-fiber_pool middleware.
5
+ # Just update your database.yml's adapter to be 'em_mysql2'
6
+
7
+ module ActiveRecord
8
+ class Base
9
+ def self.em_mysql2_connection(config)
10
+ client = ::Mysql2::Fibered::Client.new(config.symbolize_keys)
11
+ options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
12
+ ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
13
+ end
14
+ end
15
+ end
16
+
17
+ require 'fiber'
18
+ require 'eventmachine'
19
+ require 'mysql2'
20
+ require 'active_record/connection_adapters/mysql2_adapter'
21
+ require 'active_record/fiber_patches'
22
+
23
+ module Mysql2
24
+ module Fibered
25
+ class Client < ::Mysql2::Client
26
+ module Watcher
27
+ def initialize(client, deferable)
28
+ @client = client
29
+ @deferable = deferable
30
+ end
31
+
32
+ def notify_readable
33
+ begin
34
+ detach
35
+ results = @client.async_result
36
+ @deferable.succeed(results)
37
+ rescue Exception => e
38
+ @deferable.fail(e)
39
+ end
40
+ end
41
+ end
42
+
43
+ def query(sql, opts={})
44
+ if ::EM.reactor_running?
45
+ super(sql, opts.merge(:async => true))
46
+ deferrable = ::EM::DefaultDeferrable.new
47
+ ::EM.watch(self.socket, Watcher, self, deferrable).notify_readable = true
48
+ fiber = Fiber.current
49
+ deferrable.callback do |result|
50
+ fiber.resume(result)
51
+ end
52
+ deferrable.errback do |err|
53
+ fiber.resume(err)
54
+ end
55
+ Fiber.yield.tap do |result|
56
+ raise result if result.is_a?(Exception)
57
+ end
58
+ else
59
+ super(sql, opts)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,654 @@
1
+ # encoding: utf-8
2
+
3
+ require 'mysql2'
4
+
5
+ module ActiveRecord
6
+ class Base
7
+ def self.mysql2_connection(config)
8
+ config[:username] = 'root' if config[:username].nil?
9
+
10
+ if Mysql2::Client.const_defined? :FOUND_ROWS
11
+ config[:flags] = Mysql2::Client::FOUND_ROWS
12
+ end
13
+
14
+ client = Mysql2::Client.new(config.symbolize_keys)
15
+ options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
16
+ ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
17
+ end
18
+ end
19
+
20
+ module ConnectionAdapters
21
+ class Mysql2IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc:
22
+ end
23
+
24
+ class Mysql2Column < Column
25
+ BOOL = "tinyint(1)"
26
+ def extract_default(default)
27
+ if sql_type =~ /blob/i || type == :text
28
+ if default.blank?
29
+ return null ? nil : ''
30
+ else
31
+ raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
32
+ end
33
+ elsif missing_default_forged_as_empty_string?(default)
34
+ nil
35
+ else
36
+ super
37
+ end
38
+ end
39
+
40
+ def has_default?
41
+ return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
42
+ super
43
+ end
44
+
45
+ # Returns the Ruby class that corresponds to the abstract data type.
46
+ def klass
47
+ case type
48
+ when :integer then Fixnum
49
+ when :float then Float
50
+ when :decimal then BigDecimal
51
+ when :datetime then Time
52
+ when :date then Date
53
+ when :timestamp then Time
54
+ when :time then Time
55
+ when :text, :string then String
56
+ when :binary then String
57
+ when :boolean then Object
58
+ end
59
+ end
60
+
61
+ def type_cast(value)
62
+ return nil if value.nil?
63
+ case type
64
+ when :string then value
65
+ when :text then value
66
+ when :integer then value.to_i rescue value ? 1 : 0
67
+ when :float then value.to_f # returns self if it's already a Float
68
+ when :decimal then self.class.value_to_decimal(value)
69
+ when :datetime, :timestamp then value.class == Time ? value : self.class.string_to_time(value)
70
+ when :time then value.class == Time ? value : self.class.string_to_dummy_time(value)
71
+ when :date then value.class == Date ? value : self.class.string_to_date(value)
72
+ when :binary then value
73
+ when :boolean then self.class.value_to_boolean(value)
74
+ else value
75
+ end
76
+ end
77
+
78
+ def type_cast_code(var_name)
79
+ case type
80
+ when :string then nil
81
+ when :text then nil
82
+ when :integer then "#{var_name}.to_i rescue #{var_name} ? 1 : 0"
83
+ when :float then "#{var_name}.to_f"
84
+ when :decimal then "#{self.class.name}.value_to_decimal(#{var_name})"
85
+ when :datetime, :timestamp then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_time(#{var_name})"
86
+ when :time then "#{var_name}.class == Time ? #{var_name} : #{self.class.name}.string_to_dummy_time(#{var_name})"
87
+ when :date then "#{var_name}.class == Date ? #{var_name} : #{self.class.name}.string_to_date(#{var_name})"
88
+ when :binary then nil
89
+ when :boolean then "#{self.class.name}.value_to_boolean(#{var_name})"
90
+ else nil
91
+ end
92
+ end
93
+
94
+ private
95
+ def simplified_type(field_type)
96
+ return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL)
97
+ return :string if field_type =~ /enum/i or field_type =~ /set/i
98
+ return :integer if field_type =~ /year/i
99
+ return :binary if field_type =~ /bit/i
100
+ super
101
+ end
102
+
103
+ def extract_limit(sql_type)
104
+ case sql_type
105
+ when /blob|text/i
106
+ case sql_type
107
+ when /tiny/i
108
+ 255
109
+ when /medium/i
110
+ 16777215
111
+ when /long/i
112
+ 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
113
+ else
114
+ super # we could return 65535 here, but we leave it undecorated by default
115
+ end
116
+ when /^bigint/i; 8
117
+ when /^int/i; 4
118
+ when /^mediumint/i; 3
119
+ when /^smallint/i; 2
120
+ when /^tinyint/i; 1
121
+ else
122
+ super
123
+ end
124
+ end
125
+
126
+ # MySQL misreports NOT NULL column default when none is given.
127
+ # We can't detect this for columns which may have a legitimate ''
128
+ # default (string) but we can for others (integer, datetime, boolean,
129
+ # and the rest).
130
+ #
131
+ # Test whether the column has default '', is not null, and is not
132
+ # a type allowing default ''.
133
+ def missing_default_forged_as_empty_string?(default)
134
+ type != :string && !null && default == ''
135
+ end
136
+ end
137
+
138
+ class Mysql2Adapter < AbstractAdapter
139
+ cattr_accessor :emulate_booleans
140
+ self.emulate_booleans = true
141
+
142
+ ADAPTER_NAME = 'Mysql2'
143
+ PRIMARY = "PRIMARY"
144
+
145
+ LOST_CONNECTION_ERROR_MESSAGES = [
146
+ "Server shutdown in progress",
147
+ "Broken pipe",
148
+ "Lost connection to MySQL server during query",
149
+ "MySQL server has gone away" ]
150
+
151
+ QUOTED_TRUE, QUOTED_FALSE = '1', '0'
152
+
153
+ NATIVE_DATABASE_TYPES = {
154
+ :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
155
+ :string => { :name => "varchar", :limit => 255 },
156
+ :text => { :name => "text" },
157
+ :integer => { :name => "int", :limit => 4 },
158
+ :float => { :name => "float" },
159
+ :decimal => { :name => "decimal" },
160
+ :datetime => { :name => "datetime" },
161
+ :timestamp => { :name => "datetime" },
162
+ :time => { :name => "time" },
163
+ :date => { :name => "date" },
164
+ :binary => { :name => "blob" },
165
+ :boolean => { :name => "tinyint", :limit => 1 }
166
+ }
167
+
168
+ def initialize(connection, logger, connection_options, config)
169
+ super(connection, logger)
170
+ @connection_options, @config = connection_options, config
171
+ @quoted_column_names, @quoted_table_names = {}, {}
172
+ configure_connection
173
+ end
174
+
175
+ def adapter_name
176
+ ADAPTER_NAME
177
+ end
178
+
179
+ def supports_migrations?
180
+ true
181
+ end
182
+
183
+ def supports_primary_key?
184
+ true
185
+ end
186
+
187
+ def supports_savepoints?
188
+ true
189
+ end
190
+
191
+ def native_database_types
192
+ NATIVE_DATABASE_TYPES
193
+ end
194
+
195
+ # QUOTING ==================================================
196
+
197
+ def quote(value, column = nil)
198
+ if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
199
+ s = column.class.string_to_binary(value).unpack("H*")[0]
200
+ "x'#{s}'"
201
+ elsif value.kind_of?(BigDecimal)
202
+ value.to_s("F")
203
+ else
204
+ super
205
+ end
206
+ end
207
+
208
+ def quote_column_name(name) #:nodoc:
209
+ @quoted_column_names[name] ||= "`#{name}`"
210
+ end
211
+
212
+ def quote_table_name(name) #:nodoc:
213
+ @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
214
+ end
215
+
216
+ def quote_string(string)
217
+ @connection.escape(string)
218
+ end
219
+
220
+ def quoted_true
221
+ QUOTED_TRUE
222
+ end
223
+
224
+ def quoted_false
225
+ QUOTED_FALSE
226
+ end
227
+
228
+ # REFERENTIAL INTEGRITY ====================================
229
+
230
+ def disable_referential_integrity(&block) #:nodoc:
231
+ old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
232
+
233
+ begin
234
+ update("SET FOREIGN_KEY_CHECKS = 0")
235
+ yield
236
+ ensure
237
+ update("SET FOREIGN_KEY_CHECKS = #{old}")
238
+ end
239
+ end
240
+
241
+ # CONNECTION MANAGEMENT ====================================
242
+
243
+ def active?
244
+ return false unless @connection
245
+ @connection.ping
246
+ end
247
+
248
+ def reconnect!
249
+ disconnect!
250
+ connect
251
+ end
252
+
253
+ # this is set to true in 2.3, but we don't want it to be
254
+ def requires_reloading?
255
+ false
256
+ end
257
+
258
+ def disconnect!
259
+ unless @connection.nil?
260
+ @connection.close
261
+ @connection = nil
262
+ end
263
+ end
264
+
265
+ def reset!
266
+ disconnect!
267
+ connect
268
+ end
269
+
270
+ # DATABASE STATEMENTS ======================================
271
+
272
+ # FIXME: re-enable the following once a "better" query_cache solution is in core
273
+ #
274
+ # The overrides below perform much better than the originals in AbstractAdapter
275
+ # because we're able to take advantage of mysql2's lazy-loading capabilities
276
+ #
277
+ # # Returns a record hash with the column names as keys and column values
278
+ # # as values.
279
+ # def select_one(sql, name = nil)
280
+ # result = execute(sql, name)
281
+ # result.each(:as => :hash) do |r|
282
+ # return r
283
+ # end
284
+ # end
285
+ #
286
+ # # Returns a single value from a record
287
+ # def select_value(sql, name = nil)
288
+ # result = execute(sql, name)
289
+ # if first = result.first
290
+ # first.first
291
+ # end
292
+ # end
293
+ #
294
+ # # Returns an array of the values of the first column in a select:
295
+ # # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
296
+ # def select_values(sql, name = nil)
297
+ # execute(sql, name).map { |row| row.first }
298
+ # end
299
+
300
+ # Returns an array of arrays containing the field values.
301
+ # Order is the same as that returned by +columns+.
302
+ def select_rows(sql, name = nil)
303
+ execute(sql, name).to_a
304
+ end
305
+
306
+ # Executes the SQL statement in the context of this connection.
307
+ def execute(sql, name = nil)
308
+ # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
309
+ # made since we established the connection
310
+ @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
311
+ if name == :skip_logging
312
+ @connection.query(sql)
313
+ else
314
+ log(sql, name) { @connection.query(sql) }
315
+ end
316
+ rescue ActiveRecord::StatementInvalid => exception
317
+ if exception.message.split(":").first =~ /Packets out of order/
318
+ 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."
319
+ else
320
+ raise
321
+ end
322
+ end
323
+
324
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
325
+ super
326
+ id_value || @connection.last_id
327
+ end
328
+ alias :create :insert_sql
329
+
330
+ def update_sql(sql, name = nil)
331
+ super
332
+ @connection.affected_rows
333
+ end
334
+
335
+ def begin_db_transaction
336
+ execute "BEGIN"
337
+ rescue Exception
338
+ # Transactions aren't supported
339
+ end
340
+
341
+ def commit_db_transaction
342
+ execute "COMMIT"
343
+ rescue Exception
344
+ # Transactions aren't supported
345
+ end
346
+
347
+ def rollback_db_transaction
348
+ execute "ROLLBACK"
349
+ rescue Exception
350
+ # Transactions aren't supported
351
+ end
352
+
353
+ def create_savepoint
354
+ execute("SAVEPOINT #{current_savepoint_name}")
355
+ end
356
+
357
+ def rollback_to_savepoint
358
+ execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
359
+ end
360
+
361
+ def release_savepoint
362
+ execute("RELEASE SAVEPOINT #{current_savepoint_name}")
363
+ end
364
+
365
+ def add_limit_offset!(sql, options)
366
+ limit, offset = options[:limit], options[:offset]
367
+ if limit && offset
368
+ sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}"
369
+ elsif limit
370
+ sql << " LIMIT #{sanitize_limit(limit)}"
371
+ elsif offset
372
+ sql << " OFFSET #{offset.to_i}"
373
+ end
374
+ sql
375
+ end
376
+
377
+ # SCHEMA STATEMENTS ========================================
378
+
379
+ def structure_dump
380
+ if supports_views?
381
+ sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
382
+ else
383
+ sql = "SHOW TABLES"
384
+ end
385
+
386
+ select_all(sql).inject("") do |structure, table|
387
+ table.delete('Table_type')
388
+ structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n"
389
+ end
390
+ end
391
+
392
+ def recreate_database(name, options = {})
393
+ drop_database(name)
394
+ create_database(name, options)
395
+ end
396
+
397
+ # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
398
+ # Charset defaults to utf8.
399
+ #
400
+ # Example:
401
+ # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
402
+ # create_database 'matt_development'
403
+ # create_database 'matt_development', :charset => :big5
404
+ def create_database(name, options = {})
405
+ if options[:collation]
406
+ execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
407
+ else
408
+ execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
409
+ end
410
+ end
411
+
412
+ def drop_database(name) #:nodoc:
413
+ execute "DROP DATABASE IF EXISTS `#{name}`"
414
+ end
415
+
416
+ def current_database
417
+ select_value 'SELECT DATABASE() as db'
418
+ end
419
+
420
+ # Returns the database character set.
421
+ def charset
422
+ show_variable 'character_set_database'
423
+ end
424
+
425
+ # Returns the database collation strategy.
426
+ def collation
427
+ show_variable 'collation_database'
428
+ end
429
+
430
+ def tables(name = nil)
431
+ tables = []
432
+ execute("SHOW TABLES", name).each do |field|
433
+ tables << field.first
434
+ end
435
+ tables
436
+ end
437
+
438
+ def drop_table(table_name, options = {})
439
+ super(table_name, options)
440
+ end
441
+
442
+ def indexes(table_name, name = nil)
443
+ indexes = []
444
+ current_index = nil
445
+ result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name)
446
+ result.each(:symbolize_keys => true, :as => :hash) do |row|
447
+ if current_index != row[:Key_name]
448
+ next if row[:Key_name] == PRIMARY # skip the primary key
449
+ current_index = row[:Key_name]
450
+ indexes << Mysql2IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, [], [])
451
+ end
452
+
453
+ indexes.last.columns << row[:Column_name]
454
+ indexes.last.lengths << row[:Sub_part]
455
+ end
456
+ indexes
457
+ end
458
+
459
+ def columns(table_name, name = nil)
460
+ sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
461
+ columns = []
462
+ result = execute(sql, :skip_logging)
463
+ result.each(:symbolize_keys => true, :as => :hash) { |field|
464
+ columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES")
465
+ }
466
+ columns
467
+ end
468
+
469
+ def create_table(table_name, options = {})
470
+ super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
471
+ end
472
+
473
+ def rename_table(table_name, new_name)
474
+ execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
475
+ end
476
+
477
+ def add_column(table_name, column_name, type, options = {})
478
+ add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
479
+ add_column_options!(add_column_sql, options)
480
+ add_column_position!(add_column_sql, options)
481
+ execute(add_column_sql)
482
+ end
483
+
484
+ def change_column_default(table_name, column_name, default)
485
+ column = column_for(table_name, column_name)
486
+ change_column table_name, column_name, column.sql_type, :default => default
487
+ end
488
+
489
+ def change_column_null(table_name, column_name, null, default = nil)
490
+ column = column_for(table_name, column_name)
491
+
492
+ unless null || default.nil?
493
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
494
+ end
495
+
496
+ change_column table_name, column_name, column.sql_type, :null => null
497
+ end
498
+
499
+ def change_column(table_name, column_name, type, options = {})
500
+ column = column_for(table_name, column_name)
501
+
502
+ unless options_include_default?(options)
503
+ options[:default] = column.default
504
+ end
505
+
506
+ unless options.has_key?(:null)
507
+ options[:null] = column.null
508
+ end
509
+
510
+ 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])}"
511
+ add_column_options!(change_column_sql, options)
512
+ add_column_position!(change_column_sql, options)
513
+ execute(change_column_sql)
514
+ end
515
+
516
+ def rename_column(table_name, column_name, new_column_name)
517
+ options = {}
518
+ if column = columns(table_name).find { |c| c.name == column_name.to_s }
519
+ options[:default] = column.default
520
+ options[:null] = column.null
521
+ else
522
+ raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
523
+ end
524
+ current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
525
+ rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
526
+ add_column_options!(rename_column_sql, options)
527
+ execute(rename_column_sql)
528
+ end
529
+
530
+ # Maps logical Rails types to MySQL-specific data types.
531
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
532
+ return super unless type.to_s == 'integer'
533
+
534
+ case limit
535
+ when 1; 'tinyint'
536
+ when 2; 'smallint'
537
+ when 3; 'mediumint'
538
+ when nil, 4, 11; 'int(11)' # compatibility with MySQL default
539
+ when 5..8; 'bigint'
540
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}")
541
+ end
542
+ end
543
+
544
+ def add_column_position!(sql, options)
545
+ if options[:first]
546
+ sql << " FIRST"
547
+ elsif options[:after]
548
+ sql << " AFTER #{quote_column_name(options[:after])}"
549
+ end
550
+ end
551
+
552
+ def show_variable(name)
553
+ variables = select_all("SHOW VARIABLES LIKE '#{name}'")
554
+ variables.first['Value'] unless variables.empty?
555
+ end
556
+
557
+ def pk_and_sequence_for(table)
558
+ keys = []
559
+ result = execute("describe #{quote_table_name(table)}")
560
+ result.each(:symbolize_keys => true, :as => :hash) do |row|
561
+ keys << row[:Field] if row[:Key] == "PRI"
562
+ end
563
+ keys.length == 1 ? [keys.first, nil] : nil
564
+ end
565
+
566
+ # Returns just a table's primary key
567
+ def primary_key(table)
568
+ pk_and_sequence = pk_and_sequence_for(table)
569
+ pk_and_sequence && pk_and_sequence.first
570
+ end
571
+
572
+ def case_sensitive_equality_operator
573
+ "= BINARY"
574
+ end
575
+
576
+ def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
577
+ where_sql
578
+ end
579
+
580
+ protected
581
+ def quoted_columns_for_index(column_names, options = {})
582
+ length = options[:length] if options.is_a?(Hash)
583
+
584
+ quoted_column_names = case length
585
+ when Hash
586
+ column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
587
+ when Fixnum
588
+ column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
589
+ else
590
+ column_names.map {|name| quote_column_name(name) }
591
+ end
592
+ end
593
+
594
+ def translate_exception(exception, message)
595
+ return super unless exception.respond_to?(:error_number)
596
+
597
+ case exception.error_number
598
+ when 1062
599
+ RecordNotUnique.new(message, exception)
600
+ when 1452
601
+ InvalidForeignKey.new(message, exception)
602
+ else
603
+ super
604
+ end
605
+ end
606
+
607
+ private
608
+ def connect
609
+ @connection = Mysql2::Client.new(@config)
610
+ configure_connection
611
+ end
612
+
613
+ def configure_connection
614
+ @connection.query_options.merge!(:as => :array)
615
+
616
+ # By default, MySQL 'where id is null' selects the last inserted id.
617
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
618
+ variable_assignments = ['SQL_AUTO_IS_NULL=0']
619
+ encoding = @config[:encoding]
620
+
621
+ # make sure we set the encoding
622
+ variable_assignments << "NAMES '#{encoding}'" if encoding
623
+
624
+ # increase timeout so mysql server doesn't disconnect us
625
+ wait_timeout = @config[:wait_timeout]
626
+ wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum)
627
+ variable_assignments << "@@wait_timeout = #{wait_timeout}"
628
+
629
+ execute("SET #{variable_assignments.join(', ')}", :skip_logging)
630
+ end
631
+
632
+ # Returns an array of record hashes with the column names as keys and
633
+ # column values as values.
634
+ def select(sql, name = nil)
635
+ execute(sql, name).each(:as => :hash)
636
+ end
637
+
638
+ def supports_views?
639
+ version[0] >= 5
640
+ end
641
+
642
+ def version
643
+ @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
644
+ end
645
+
646
+ def column_for(table_name, column_name)
647
+ unless column = columns(table_name).find { |c| c.name == column_name.to_s }
648
+ raise "No such column: #{table_name}.#{column_name}"
649
+ end
650
+ column
651
+ end
652
+ end
653
+ end
654
+ end