activerecord 3.0.20 → 3.1.0.beta1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (122) hide show
  1. data/CHANGELOG +220 -91
  2. data/README.rdoc +3 -3
  3. data/examples/performance.rb +88 -109
  4. data/lib/active_record.rb +6 -2
  5. data/lib/active_record/aggregations.rb +22 -45
  6. data/lib/active_record/associations.rb +264 -991
  7. data/lib/active_record/associations/alias_tracker.rb +85 -0
  8. data/lib/active_record/associations/association.rb +231 -0
  9. data/lib/active_record/associations/association_scope.rb +120 -0
  10. data/lib/active_record/associations/belongs_to_association.rb +40 -60
  11. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +15 -63
  12. data/lib/active_record/associations/builder/association.rb +53 -0
  13. data/lib/active_record/associations/builder/belongs_to.rb +85 -0
  14. data/lib/active_record/associations/builder/collection_association.rb +75 -0
  15. data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +63 -0
  16. data/lib/active_record/associations/builder/has_many.rb +65 -0
  17. data/lib/active_record/associations/builder/has_one.rb +63 -0
  18. data/lib/active_record/associations/builder/singular_association.rb +32 -0
  19. data/lib/active_record/associations/collection_association.rb +524 -0
  20. data/lib/active_record/associations/collection_proxy.rb +125 -0
  21. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +27 -118
  22. data/lib/active_record/associations/has_many_association.rb +50 -79
  23. data/lib/active_record/associations/has_many_through_association.rb +98 -67
  24. data/lib/active_record/associations/has_one_association.rb +45 -115
  25. data/lib/active_record/associations/has_one_through_association.rb +21 -25
  26. data/lib/active_record/associations/join_dependency.rb +215 -0
  27. data/lib/active_record/associations/join_dependency/join_association.rb +150 -0
  28. data/lib/active_record/associations/join_dependency/join_base.rb +24 -0
  29. data/lib/active_record/associations/join_dependency/join_part.rb +78 -0
  30. data/lib/active_record/associations/join_helper.rb +56 -0
  31. data/lib/active_record/associations/preloader.rb +177 -0
  32. data/lib/active_record/associations/preloader/association.rb +126 -0
  33. data/lib/active_record/associations/preloader/belongs_to.rb +17 -0
  34. data/lib/active_record/associations/preloader/collection_association.rb +24 -0
  35. data/lib/active_record/associations/preloader/has_and_belongs_to_many.rb +60 -0
  36. data/lib/active_record/associations/preloader/has_many.rb +17 -0
  37. data/lib/active_record/associations/preloader/has_many_through.rb +15 -0
  38. data/lib/active_record/associations/preloader/has_one.rb +23 -0
  39. data/lib/active_record/associations/preloader/has_one_through.rb +9 -0
  40. data/lib/active_record/associations/preloader/singular_association.rb +21 -0
  41. data/lib/active_record/associations/preloader/through_association.rb +67 -0
  42. data/lib/active_record/associations/singular_association.rb +55 -0
  43. data/lib/active_record/associations/through_association.rb +80 -0
  44. data/lib/active_record/attribute_methods.rb +19 -5
  45. data/lib/active_record/attribute_methods/before_type_cast.rb +9 -8
  46. data/lib/active_record/attribute_methods/dirty.rb +8 -2
  47. data/lib/active_record/attribute_methods/primary_key.rb +33 -13
  48. data/lib/active_record/attribute_methods/read.rb +17 -17
  49. data/lib/active_record/attribute_methods/time_zone_conversion.rb +7 -4
  50. data/lib/active_record/attribute_methods/write.rb +2 -1
  51. data/lib/active_record/autosave_association.rb +66 -45
  52. data/lib/active_record/base.rb +445 -273
  53. data/lib/active_record/callbacks.rb +24 -33
  54. data/lib/active_record/coders/yaml_column.rb +41 -0
  55. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +106 -13
  56. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +16 -2
  57. data/lib/active_record/connection_adapters/abstract/database_limits.rb +12 -11
  58. data/lib/active_record/connection_adapters/abstract/database_statements.rb +83 -12
  59. data/lib/active_record/connection_adapters/abstract/query_cache.rb +16 -16
  60. data/lib/active_record/connection_adapters/abstract/quoting.rb +61 -22
  61. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +16 -273
  62. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +80 -42
  63. data/lib/active_record/connection_adapters/abstract_adapter.rb +44 -25
  64. data/lib/active_record/connection_adapters/column.rb +268 -0
  65. data/lib/active_record/connection_adapters/mysql2_adapter.rb +686 -0
  66. data/lib/active_record/connection_adapters/mysql_adapter.rb +331 -88
  67. data/lib/active_record/connection_adapters/postgresql_adapter.rb +295 -267
  68. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +3 -7
  69. data/lib/active_record/connection_adapters/sqlite_adapter.rb +108 -26
  70. data/lib/active_record/counter_cache.rb +7 -4
  71. data/lib/active_record/fixtures.rb +174 -192
  72. data/lib/active_record/identity_map.rb +131 -0
  73. data/lib/active_record/locking/optimistic.rb +20 -14
  74. data/lib/active_record/locking/pessimistic.rb +4 -4
  75. data/lib/active_record/log_subscriber.rb +24 -4
  76. data/lib/active_record/migration.rb +265 -144
  77. data/lib/active_record/migration/command_recorder.rb +103 -0
  78. data/lib/active_record/named_scope.rb +68 -25
  79. data/lib/active_record/nested_attributes.rb +58 -15
  80. data/lib/active_record/observer.rb +3 -7
  81. data/lib/active_record/persistence.rb +58 -38
  82. data/lib/active_record/query_cache.rb +25 -3
  83. data/lib/active_record/railtie.rb +21 -12
  84. data/lib/active_record/railties/console_sandbox.rb +6 -0
  85. data/lib/active_record/railties/databases.rake +147 -116
  86. data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
  87. data/lib/active_record/reflection.rb +176 -44
  88. data/lib/active_record/relation.rb +125 -49
  89. data/lib/active_record/relation/batches.rb +7 -5
  90. data/lib/active_record/relation/calculations.rb +50 -18
  91. data/lib/active_record/relation/finder_methods.rb +47 -26
  92. data/lib/active_record/relation/predicate_builder.rb +24 -21
  93. data/lib/active_record/relation/query_methods.rb +117 -101
  94. data/lib/active_record/relation/spawn_methods.rb +27 -20
  95. data/lib/active_record/result.rb +34 -0
  96. data/lib/active_record/schema.rb +5 -6
  97. data/lib/active_record/schema_dumper.rb +11 -13
  98. data/lib/active_record/serialization.rb +2 -2
  99. data/lib/active_record/serializers/xml_serializer.rb +10 -10
  100. data/lib/active_record/session_store.rb +8 -2
  101. data/lib/active_record/test_case.rb +9 -20
  102. data/lib/active_record/timestamp.rb +21 -9
  103. data/lib/active_record/transactions.rb +16 -15
  104. data/lib/active_record/validations.rb +21 -22
  105. data/lib/active_record/validations/associated.rb +3 -1
  106. data/lib/active_record/validations/uniqueness.rb +48 -58
  107. data/lib/active_record/version.rb +3 -3
  108. data/lib/rails/generators/active_record.rb +6 -0
  109. data/lib/rails/generators/active_record/migration/templates/migration.rb +10 -2
  110. data/lib/rails/generators/active_record/model/model_generator.rb +2 -1
  111. data/lib/rails/generators/active_record/model/templates/migration.rb +6 -5
  112. data/lib/rails/generators/active_record/model/templates/model.rb +2 -0
  113. data/lib/rails/generators/active_record/model/templates/module.rb +2 -0
  114. data/lib/rails/generators/active_record/observer/templates/observer.rb +2 -0
  115. data/lib/rails/generators/active_record/session_migration/session_migration_generator.rb +2 -1
  116. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +2 -2
  117. metadata +106 -77
  118. checksums.yaml +0 -7
  119. data/lib/active_record/association_preload.rb +0 -431
  120. data/lib/active_record/associations/association_collection.rb +0 -572
  121. data/lib/active_record/associations/association_proxy.rb +0 -304
  122. data/lib/active_record/associations/through_association_scope.rb +0 -160
@@ -0,0 +1,686 @@
1
+ # encoding: utf-8
2
+
3
+ gem 'mysql2', '~> 0.3.0'
4
+ require 'mysql2'
5
+
6
+ module ActiveRecord
7
+ class Base
8
+ # Establishes a connection to the database that's used by all Active Record objects.
9
+ def self.mysql2_connection(config)
10
+ config[:username] = 'root' if config[:username].nil?
11
+
12
+ if Mysql2::Client.const_defined? :FOUND_ROWS
13
+ config[:flags] = Mysql2::Client::FOUND_ROWS
14
+ end
15
+
16
+ client = Mysql2::Client.new(config.symbolize_keys)
17
+ options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
18
+ ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
19
+ end
20
+ end
21
+
22
+ module ConnectionAdapters
23
+ class Mysql2IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc:
24
+ end
25
+
26
+ class Mysql2Column < Column
27
+ BOOL = "tinyint(1)"
28
+ def extract_default(default)
29
+ if sql_type =~ /blob/i || type == :text
30
+ if default.blank?
31
+ return null ? nil : ''
32
+ else
33
+ raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
34
+ end
35
+ elsif missing_default_forged_as_empty_string?(default)
36
+ nil
37
+ else
38
+ super
39
+ end
40
+ end
41
+
42
+ def has_default?
43
+ return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
44
+ super
45
+ end
46
+
47
+ private
48
+ def simplified_type(field_type)
49
+ return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL)
50
+
51
+ case field_type
52
+ when /enum/i, /set/i then :string
53
+ when /year/i then :integer
54
+ when /bit/i then :binary
55
+ else
56
+ super
57
+ end
58
+ end
59
+
60
+ def extract_limit(sql_type)
61
+ case sql_type
62
+ when /blob|text/i
63
+ case sql_type
64
+ when /tiny/i
65
+ 255
66
+ when /medium/i
67
+ 16777215
68
+ when /long/i
69
+ 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
70
+ else
71
+ super # we could return 65535 here, but we leave it undecorated by default
72
+ end
73
+ when /^bigint/i; 8
74
+ when /^int/i; 4
75
+ when /^mediumint/i; 3
76
+ when /^smallint/i; 2
77
+ when /^tinyint/i; 1
78
+ else
79
+ super
80
+ end
81
+ end
82
+
83
+ # MySQL misreports NOT NULL column default when none is given.
84
+ # We can't detect this for columns which may have a legitimate ''
85
+ # default (string) but we can for others (integer, datetime, boolean,
86
+ # and the rest).
87
+ #
88
+ # Test whether the column has default '', is not null, and is not
89
+ # a type allowing default ''.
90
+ def missing_default_forged_as_empty_string?(default)
91
+ type != :string && !null && default == ''
92
+ end
93
+ end
94
+
95
+ class Mysql2Adapter < AbstractAdapter
96
+ cattr_accessor :emulate_booleans
97
+ self.emulate_booleans = true
98
+
99
+ ADAPTER_NAME = 'Mysql2'
100
+ PRIMARY = "PRIMARY"
101
+
102
+ LOST_CONNECTION_ERROR_MESSAGES = [
103
+ "Server shutdown in progress",
104
+ "Broken pipe",
105
+ "Lost connection to MySQL server during query",
106
+ "MySQL server has gone away" ]
107
+
108
+ QUOTED_TRUE, QUOTED_FALSE = '1', '0'
109
+
110
+ NATIVE_DATABASE_TYPES = {
111
+ :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
112
+ :string => { :name => "varchar", :limit => 255 },
113
+ :text => { :name => "text" },
114
+ :integer => { :name => "int", :limit => 4 },
115
+ :float => { :name => "float" },
116
+ :decimal => { :name => "decimal" },
117
+ :datetime => { :name => "datetime" },
118
+ :timestamp => { :name => "datetime" },
119
+ :time => { :name => "time" },
120
+ :date => { :name => "date" },
121
+ :binary => { :name => "blob" },
122
+ :boolean => { :name => "tinyint", :limit => 1 }
123
+ }
124
+
125
+ def initialize(connection, logger, connection_options, config)
126
+ super(connection, logger)
127
+ @connection_options, @config = connection_options, config
128
+ @quoted_column_names, @quoted_table_names = {}, {}
129
+ configure_connection
130
+ end
131
+
132
+ def adapter_name
133
+ ADAPTER_NAME
134
+ end
135
+
136
+ # Returns true, since this connection adapter supports migrations.
137
+ def supports_migrations?
138
+ true
139
+ end
140
+
141
+ def supports_primary_key?
142
+ true
143
+ end
144
+
145
+ # Returns true, since this connection adapter supports savepoints.
146
+ def supports_savepoints?
147
+ true
148
+ end
149
+
150
+ def native_database_types
151
+ NATIVE_DATABASE_TYPES
152
+ end
153
+
154
+ # QUOTING ==================================================
155
+
156
+ def quote(value, column = nil)
157
+ if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
158
+ s = column.class.string_to_binary(value).unpack("H*")[0]
159
+ "x'#{s}'"
160
+ elsif value.kind_of?(BigDecimal)
161
+ value.to_s("F")
162
+ else
163
+ super
164
+ end
165
+ end
166
+
167
+ def quote_column_name(name) #:nodoc:
168
+ @quoted_column_names[name] ||= "`#{name}`"
169
+ end
170
+
171
+ def quote_table_name(name) #:nodoc:
172
+ @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
173
+ end
174
+
175
+ def quote_string(string)
176
+ @connection.escape(string)
177
+ end
178
+
179
+ def quoted_true
180
+ QUOTED_TRUE
181
+ end
182
+
183
+ def quoted_false
184
+ QUOTED_FALSE
185
+ end
186
+
187
+ # REFERENTIAL INTEGRITY ====================================
188
+
189
+ def disable_referential_integrity(&block) #:nodoc:
190
+ old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
191
+
192
+ begin
193
+ update("SET FOREIGN_KEY_CHECKS = 0")
194
+ yield
195
+ ensure
196
+ update("SET FOREIGN_KEY_CHECKS = #{old}")
197
+ end
198
+ end
199
+
200
+ # CONNECTION MANAGEMENT ====================================
201
+
202
+ def active?
203
+ return false unless @connection
204
+ @connection.ping
205
+ end
206
+
207
+ def reconnect!
208
+ disconnect!
209
+ connect
210
+ end
211
+
212
+ # this is set to true in 2.3, but we don't want it to be
213
+ def requires_reloading?
214
+ false
215
+ end
216
+
217
+ # Disconnects from the database if already connected.
218
+ # Otherwise, this method does nothing.
219
+ def disconnect!
220
+ unless @connection.nil?
221
+ @connection.close
222
+ @connection = nil
223
+ end
224
+ end
225
+
226
+ def reset!
227
+ disconnect!
228
+ connect
229
+ end
230
+
231
+ # DATABASE STATEMENTS ======================================
232
+
233
+ # FIXME: re-enable the following once a "better" query_cache solution is in core
234
+ #
235
+ # The overrides below perform much better than the originals in AbstractAdapter
236
+ # because we're able to take advantage of mysql2's lazy-loading capabilities
237
+ #
238
+ # # Returns a record hash with the column names as keys and column values
239
+ # # as values.
240
+ # def select_one(sql, name = nil)
241
+ # result = execute(sql, name)
242
+ # result.each(:as => :hash) do |r|
243
+ # return r
244
+ # end
245
+ # end
246
+ #
247
+ # # Returns a single value from a record
248
+ # def select_value(sql, name = nil)
249
+ # result = execute(sql, name)
250
+ # if first = result.first
251
+ # first.first
252
+ # end
253
+ # end
254
+ #
255
+ # # Returns an array of the values of the first column in a select:
256
+ # # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
257
+ # def select_values(sql, name = nil)
258
+ # execute(sql, name).map { |row| row.first }
259
+ # end
260
+
261
+ # Returns an array of arrays containing the field values.
262
+ # Order is the same as that returned by +columns+.
263
+ def select_rows(sql, name = nil)
264
+ execute(sql, name).to_a
265
+ end
266
+
267
+ # Executes the SQL statement in the context of this connection.
268
+ def execute(sql, name = nil)
269
+ # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
270
+ # made since we established the connection
271
+ @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
272
+ if name == :skip_logging
273
+ @connection.query(sql)
274
+ else
275
+ log(sql, name) { @connection.query(sql) }
276
+ end
277
+ rescue ActiveRecord::StatementInvalid => exception
278
+ if exception.message.split(":").first =~ /Packets out of order/
279
+ 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."
280
+ else
281
+ raise
282
+ end
283
+ end
284
+
285
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
286
+ super
287
+ id_value || @connection.last_id
288
+ end
289
+ alias :create :insert_sql
290
+
291
+ def exec_insert(sql, name, binds)
292
+ binds = binds.dup
293
+
294
+ # Pretend to support bind parameters
295
+ execute sql.gsub('?') { quote(*binds.shift.reverse) }, name
296
+ end
297
+
298
+ def exec_delete(sql, name, binds)
299
+ binds = binds.dup
300
+
301
+ # Pretend to support bind parameters
302
+ execute sql.gsub('?') { quote(*binds.shift.reverse) }, name
303
+ @connection.affected_rows
304
+ end
305
+ alias :exec_update :exec_delete
306
+
307
+ def last_inserted_id(result)
308
+ @connection.last_id
309
+ end
310
+
311
+ def update_sql(sql, name = nil)
312
+ super
313
+ @connection.affected_rows
314
+ end
315
+
316
+ def begin_db_transaction
317
+ execute "BEGIN"
318
+ rescue Exception
319
+ # Transactions aren't supported
320
+ end
321
+
322
+ def commit_db_transaction
323
+ execute "COMMIT"
324
+ rescue Exception
325
+ # Transactions aren't supported
326
+ end
327
+
328
+ def rollback_db_transaction
329
+ execute "ROLLBACK"
330
+ rescue Exception
331
+ # Transactions aren't supported
332
+ end
333
+
334
+ def create_savepoint
335
+ execute("SAVEPOINT #{current_savepoint_name}")
336
+ end
337
+
338
+ def rollback_to_savepoint
339
+ execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
340
+ end
341
+
342
+ def release_savepoint
343
+ execute("RELEASE SAVEPOINT #{current_savepoint_name}")
344
+ end
345
+
346
+ def add_limit_offset!(sql, options)
347
+ limit, offset = options[:limit], options[:offset]
348
+ if limit && offset
349
+ sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}"
350
+ elsif limit
351
+ sql << " LIMIT #{sanitize_limit(limit)}"
352
+ elsif offset
353
+ sql << " OFFSET #{offset.to_i}"
354
+ end
355
+ sql
356
+ end
357
+ deprecate :add_limit_offset!
358
+
359
+ # SCHEMA STATEMENTS ========================================
360
+
361
+ def structure_dump
362
+ if supports_views?
363
+ sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
364
+ else
365
+ sql = "SHOW TABLES"
366
+ end
367
+
368
+ select_all(sql).inject("") do |structure, table|
369
+ table.delete('Table_type')
370
+ structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n"
371
+ end
372
+ end
373
+
374
+ # Drops the database specified on the +name+ attribute
375
+ # and creates it again using the provided +options+.
376
+ def recreate_database(name, options = {})
377
+ drop_database(name)
378
+ create_database(name, options)
379
+ end
380
+
381
+ # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
382
+ # Charset defaults to utf8.
383
+ #
384
+ # Example:
385
+ # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
386
+ # create_database 'matt_development'
387
+ # create_database 'matt_development', :charset => :big5
388
+ def create_database(name, options = {})
389
+ if options[:collation]
390
+ execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
391
+ else
392
+ execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
393
+ end
394
+ end
395
+
396
+ # Drops a MySQL database.
397
+ #
398
+ # Example:
399
+ # drop_database('sebastian_development')
400
+ def drop_database(name) #:nodoc:
401
+ execute "DROP DATABASE IF EXISTS `#{name}`"
402
+ end
403
+
404
+ def current_database
405
+ select_value 'SELECT DATABASE() as db'
406
+ end
407
+
408
+ # Returns the database character set.
409
+ def charset
410
+ show_variable 'character_set_database'
411
+ end
412
+
413
+ # Returns the database collation strategy.
414
+ def collation
415
+ show_variable 'collation_database'
416
+ end
417
+
418
+ def tables(name = nil, database = nil) #:nodoc:
419
+ sql = ["SHOW TABLES", database].compact.join(' IN ')
420
+ execute(sql, 'SCHEMA').collect do |field|
421
+ field.first
422
+ end
423
+ end
424
+
425
+ def table_exists?(name)
426
+ return true if super
427
+
428
+ name = name.to_s
429
+ schema, table = name.split('.', 2)
430
+
431
+ unless table # A table was provided without a schema
432
+ table = schema
433
+ schema = nil
434
+ end
435
+
436
+ tables(nil, schema).include? table
437
+ end
438
+
439
+ def drop_table(table_name, options = {})
440
+ super(table_name, options)
441
+ end
442
+
443
+ # Returns an array of indexes for the given table.
444
+ def indexes(table_name, name = nil)
445
+ indexes = []
446
+ current_index = nil
447
+ result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA')
448
+ result.each(:symbolize_keys => true, :as => :hash) do |row|
449
+ if current_index != row[:Key_name]
450
+ next if row[:Key_name] == PRIMARY # skip the primary key
451
+ current_index = row[:Key_name]
452
+ indexes << Mysql2IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, [], [])
453
+ end
454
+
455
+ indexes.last.columns << row[:Column_name]
456
+ indexes.last.lengths << row[:Sub_part]
457
+ end
458
+ indexes
459
+ end
460
+
461
+ # Returns an array of +Mysql2Column+ objects for the table specified by +table_name+.
462
+ def columns(table_name, name = nil)
463
+ sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
464
+ columns = []
465
+ result = execute(sql, 'SCHEMA')
466
+ result.each(:symbolize_keys => true, :as => :hash) { |field|
467
+ columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES")
468
+ }
469
+ columns
470
+ end
471
+
472
+ def create_table(table_name, options = {})
473
+ super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
474
+ end
475
+
476
+ # Renames a table.
477
+ #
478
+ # Example:
479
+ # rename_table('octopuses', 'octopi')
480
+ def rename_table(table_name, new_name)
481
+ execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
482
+ end
483
+
484
+ def add_column(table_name, column_name, type, options = {})
485
+ 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])}"
486
+ add_column_options!(add_column_sql, options)
487
+ add_column_position!(add_column_sql, options)
488
+ execute(add_column_sql)
489
+ end
490
+
491
+ def change_column_default(table_name, column_name, default)
492
+ column = column_for(table_name, column_name)
493
+ change_column table_name, column_name, column.sql_type, :default => default
494
+ end
495
+
496
+ def change_column_null(table_name, column_name, null, default = nil)
497
+ column = column_for(table_name, column_name)
498
+
499
+ unless null || default.nil?
500
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
501
+ end
502
+
503
+ change_column table_name, column_name, column.sql_type, :null => null
504
+ end
505
+
506
+ def change_column(table_name, column_name, type, options = {})
507
+ column = column_for(table_name, column_name)
508
+
509
+ unless options_include_default?(options)
510
+ options[:default] = column.default
511
+ end
512
+
513
+ unless options.has_key?(:null)
514
+ options[:null] = column.null
515
+ end
516
+
517
+ 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])}"
518
+ add_column_options!(change_column_sql, options)
519
+ add_column_position!(change_column_sql, options)
520
+ execute(change_column_sql)
521
+ end
522
+
523
+ def rename_column(table_name, column_name, new_column_name)
524
+ options = {}
525
+ if column = columns(table_name).find { |c| c.name == column_name.to_s }
526
+ options[:default] = column.default
527
+ options[:null] = column.null
528
+ else
529
+ raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
530
+ end
531
+ current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
532
+ rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
533
+ add_column_options!(rename_column_sql, options)
534
+ execute(rename_column_sql)
535
+ end
536
+
537
+ # Maps logical Rails types to MySQL-specific data types.
538
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
539
+ return super unless type.to_s == 'integer'
540
+
541
+ case limit
542
+ when 1; 'tinyint'
543
+ when 2; 'smallint'
544
+ when 3; 'mediumint'
545
+ when nil, 4, 11; 'int(11)' # compatibility with MySQL default
546
+ when 5..8; 'bigint'
547
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}")
548
+ end
549
+ end
550
+
551
+ def add_column_position!(sql, options)
552
+ if options[:first]
553
+ sql << " FIRST"
554
+ elsif options[:after]
555
+ sql << " AFTER #{quote_column_name(options[:after])}"
556
+ end
557
+ end
558
+
559
+ # SHOW VARIABLES LIKE 'name'.
560
+ def show_variable(name)
561
+ variables = select_all("SHOW VARIABLES LIKE '#{name}'")
562
+ variables.first['Value'] unless variables.empty?
563
+ end
564
+
565
+ # Returns a table's primary key and belonging sequence.
566
+ def pk_and_sequence_for(table)
567
+ keys = []
568
+ result = execute("DESCRIBE #{quote_table_name(table)}", 'SCHEMA')
569
+ result.each(:symbolize_keys => true, :as => :hash) do |row|
570
+ keys << row[:Field] if row[:Key] == "PRI"
571
+ end
572
+ keys.length == 1 ? [keys.first, nil] : nil
573
+ end
574
+
575
+ # Returns just a table's primary key
576
+ def primary_key(table)
577
+ pk_and_sequence = pk_and_sequence_for(table)
578
+ pk_and_sequence && pk_and_sequence.first
579
+ end
580
+
581
+ def case_sensitive_equality_operator
582
+ "= BINARY"
583
+ end
584
+ deprecate :case_sensitive_equality_operator
585
+
586
+ def case_sensitive_modifier(node)
587
+ Arel::Nodes::Bin.new(node)
588
+ end
589
+
590
+ def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
591
+ where_sql
592
+ end
593
+
594
+ protected
595
+ def quoted_columns_for_index(column_names, options = {})
596
+ length = options[:length] if options.is_a?(Hash)
597
+
598
+ quoted_column_names = case length
599
+ when Hash
600
+ column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
601
+ when Fixnum
602
+ column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
603
+ else
604
+ column_names.map {|name| quote_column_name(name) }
605
+ end
606
+ end
607
+
608
+ def translate_exception(exception, message)
609
+ return super unless exception.respond_to?(:error_number)
610
+
611
+ case exception.error_number
612
+ when 1062
613
+ RecordNotUnique.new(message, exception)
614
+ when 1452
615
+ InvalidForeignKey.new(message, exception)
616
+ else
617
+ super
618
+ end
619
+ end
620
+
621
+ private
622
+ def connect
623
+ @connection = Mysql2::Client.new(@config)
624
+ configure_connection
625
+ end
626
+
627
+ def configure_connection
628
+ @connection.query_options.merge!(:as => :array)
629
+
630
+ # By default, MySQL 'where id is null' selects the last inserted id.
631
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
632
+ variable_assignments = ['SQL_AUTO_IS_NULL=0']
633
+ encoding = @config[:encoding]
634
+
635
+ # make sure we set the encoding
636
+ variable_assignments << "NAMES '#{encoding}'" if encoding
637
+
638
+ # increase timeout so mysql server doesn't disconnect us
639
+ wait_timeout = @config[:wait_timeout]
640
+ wait_timeout = 2592000 unless wait_timeout.is_a?(Fixnum)
641
+ variable_assignments << "@@wait_timeout = #{wait_timeout}"
642
+
643
+ execute("SET #{variable_assignments.join(', ')}", :skip_logging)
644
+ end
645
+
646
+ # Returns an array of record hashes with the column names as keys and
647
+ # column values as values.
648
+ def select(sql, name = nil, binds = [])
649
+ exec_query(sql, name, binds).to_a
650
+ end
651
+
652
+ def exec_query(sql, name = 'SQL', binds = [])
653
+ @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
654
+
655
+ log(sql, name, binds) do
656
+ begin
657
+ result = @connection.query(sql)
658
+ rescue ActiveRecord::StatementInvalid => exception
659
+ if exception.message.split(":").first =~ /Packets out of order/
660
+ 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."
661
+ else
662
+ raise
663
+ end
664
+ end
665
+
666
+ ActiveRecord::Result.new(result.fields, result.to_a)
667
+ end
668
+ end
669
+
670
+ def supports_views?
671
+ version[0] >= 5
672
+ end
673
+
674
+ def version
675
+ @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
676
+ end
677
+
678
+ def column_for(table_name, column_name)
679
+ unless column = columns(table_name).find { |c| c.name == column_name.to_s }
680
+ raise "No such column: #{table_name}.#{column_name}"
681
+ end
682
+ column
683
+ end
684
+ end
685
+ end
686
+ end