activerecord 3.1.9 → 3.2.12

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 (112) hide show
  1. checksums.yaml +6 -6
  2. data/CHANGELOG.md +317 -336
  3. data/README.rdoc +3 -3
  4. data/examples/performance.rb +20 -1
  5. data/lib/active_record/aggregations.rb +1 -1
  6. data/lib/active_record/associations/alias_tracker.rb +3 -6
  7. data/lib/active_record/associations/association.rb +3 -42
  8. data/lib/active_record/associations/association_scope.rb +3 -15
  9. data/lib/active_record/associations/builder/association.rb +6 -4
  10. data/lib/active_record/associations/builder/belongs_to.rb +3 -3
  11. data/lib/active_record/associations/builder/collection_association.rb +2 -2
  12. data/lib/active_record/associations/builder/has_many.rb +4 -4
  13. data/lib/active_record/associations/builder/has_one.rb +5 -6
  14. data/lib/active_record/associations/builder/singular_association.rb +3 -16
  15. data/lib/active_record/associations/collection_association.rb +64 -31
  16. data/lib/active_record/associations/collection_proxy.rb +2 -37
  17. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +1 -0
  18. data/lib/active_record/associations/has_many_association.rb +5 -1
  19. data/lib/active_record/associations/has_many_through_association.rb +28 -9
  20. data/lib/active_record/associations/has_one_association.rb +15 -13
  21. data/lib/active_record/associations/join_dependency.rb +2 -2
  22. data/lib/active_record/associations/preloader.rb +14 -10
  23. data/lib/active_record/associations/through_association.rb +7 -3
  24. data/lib/active_record/associations.rb +92 -76
  25. data/lib/active_record/attribute_assignment.rb +221 -0
  26. data/lib/active_record/attribute_methods/deprecated_underscore_read.rb +32 -0
  27. data/lib/active_record/attribute_methods/dirty.rb +21 -11
  28. data/lib/active_record/attribute_methods/primary_key.rb +62 -25
  29. data/lib/active_record/attribute_methods/read.rb +73 -83
  30. data/lib/active_record/attribute_methods/serialization.rb +102 -0
  31. data/lib/active_record/attribute_methods/time_zone_conversion.rb +23 -17
  32. data/lib/active_record/attribute_methods/write.rb +31 -6
  33. data/lib/active_record/attribute_methods.rb +231 -30
  34. data/lib/active_record/autosave_association.rb +43 -22
  35. data/lib/active_record/base.rb +227 -1708
  36. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +150 -148
  37. data/lib/active_record/connection_adapters/abstract/connection_specification.rb +85 -29
  38. data/lib/active_record/connection_adapters/abstract/database_statements.rb +6 -33
  39. data/lib/active_record/connection_adapters/abstract/query_cache.rb +10 -2
  40. data/lib/active_record/connection_adapters/abstract/quoting.rb +15 -6
  41. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +37 -26
  42. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +48 -19
  43. data/lib/active_record/connection_adapters/abstract_adapter.rb +77 -42
  44. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +674 -0
  45. data/lib/active_record/connection_adapters/column.rb +37 -11
  46. data/lib/active_record/connection_adapters/mysql2_adapter.rb +129 -581
  47. data/lib/active_record/connection_adapters/mysql_adapter.rb +137 -696
  48. data/lib/active_record/connection_adapters/postgresql_adapter.rb +184 -86
  49. data/lib/active_record/connection_adapters/schema_cache.rb +50 -0
  50. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +2 -6
  51. data/lib/active_record/connection_adapters/sqlite_adapter.rb +55 -32
  52. data/lib/active_record/counter_cache.rb +9 -4
  53. data/lib/active_record/dynamic_finder_match.rb +12 -0
  54. data/lib/active_record/dynamic_matchers.rb +84 -0
  55. data/lib/active_record/errors.rb +11 -1
  56. data/lib/active_record/explain.rb +85 -0
  57. data/lib/active_record/explain_subscriber.rb +25 -0
  58. data/lib/active_record/fixtures/file.rb +65 -0
  59. data/lib/active_record/fixtures.rb +56 -85
  60. data/lib/active_record/identity_map.rb +3 -4
  61. data/lib/active_record/inheritance.rb +174 -0
  62. data/lib/active_record/integration.rb +49 -0
  63. data/lib/active_record/locking/optimistic.rb +30 -25
  64. data/lib/active_record/locking/pessimistic.rb +23 -1
  65. data/lib/active_record/log_subscriber.rb +3 -3
  66. data/lib/active_record/migration/command_recorder.rb +8 -8
  67. data/lib/active_record/migration.rb +68 -35
  68. data/lib/active_record/model_schema.rb +366 -0
  69. data/lib/active_record/nested_attributes.rb +3 -2
  70. data/lib/active_record/persistence.rb +57 -11
  71. data/lib/active_record/querying.rb +58 -0
  72. data/lib/active_record/railtie.rb +31 -29
  73. data/lib/active_record/railties/controller_runtime.rb +3 -1
  74. data/lib/active_record/railties/databases.rake +191 -110
  75. data/lib/active_record/railties/jdbcmysql_error.rb +1 -1
  76. data/lib/active_record/readonly_attributes.rb +26 -0
  77. data/lib/active_record/reflection.rb +7 -15
  78. data/lib/active_record/relation/batches.rb +5 -2
  79. data/lib/active_record/relation/calculations.rb +47 -15
  80. data/lib/active_record/relation/delegation.rb +49 -0
  81. data/lib/active_record/relation/finder_methods.rb +9 -7
  82. data/lib/active_record/relation/predicate_builder.rb +18 -7
  83. data/lib/active_record/relation/query_methods.rb +75 -9
  84. data/lib/active_record/relation/spawn_methods.rb +11 -2
  85. data/lib/active_record/relation.rb +78 -32
  86. data/lib/active_record/result.rb +1 -1
  87. data/lib/active_record/sanitization.rb +194 -0
  88. data/lib/active_record/schema_dumper.rb +12 -5
  89. data/lib/active_record/scoping/default.rb +142 -0
  90. data/lib/active_record/scoping/named.rb +202 -0
  91. data/lib/active_record/scoping.rb +152 -0
  92. data/lib/active_record/serialization.rb +1 -43
  93. data/lib/active_record/serializers/xml_serializer.rb +4 -45
  94. data/lib/active_record/session_store.rb +17 -15
  95. data/lib/active_record/store.rb +52 -0
  96. data/lib/active_record/test_case.rb +11 -7
  97. data/lib/active_record/timestamp.rb +17 -3
  98. data/lib/active_record/transactions.rb +27 -6
  99. data/lib/active_record/translation.rb +22 -0
  100. data/lib/active_record/validations/associated.rb +5 -4
  101. data/lib/active_record/validations/uniqueness.rb +7 -7
  102. data/lib/active_record/validations.rb +1 -1
  103. data/lib/active_record/version.rb +2 -2
  104. data/lib/active_record.rb +38 -3
  105. data/lib/rails/generators/active_record/migration/migration_generator.rb +1 -1
  106. data/lib/rails/generators/active_record/migration/templates/migration.rb +12 -3
  107. data/lib/rails/generators/active_record/model/model_generator.rb +9 -1
  108. data/lib/rails/generators/active_record/model/templates/migration.rb +3 -5
  109. data/lib/rails/generators/active_record/model/templates/model.rb +5 -0
  110. data/lib/rails/generators/active_record/session_migration/templates/migration.rb +1 -5
  111. metadata +30 -10
  112. data/lib/active_record/named_scope.rb +0 -200
@@ -0,0 +1,674 @@
1
+ require 'active_support/core_ext/object/blank'
2
+ require 'arel/visitors/bind_visitor'
3
+
4
+ module ActiveRecord
5
+ module ConnectionAdapters
6
+ class AbstractMysqlAdapter < AbstractAdapter
7
+ class Column < ConnectionAdapters::Column # :nodoc:
8
+ attr_reader :collation
9
+
10
+ def initialize(name, default, sql_type = nil, null = true, collation = nil)
11
+ super(name, default, sql_type, null)
12
+ @collation = collation
13
+ end
14
+
15
+ def extract_default(default)
16
+ if sql_type =~ /blob/i || type == :text
17
+ if default.blank?
18
+ return null ? nil : ''
19
+ else
20
+ raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
21
+ end
22
+ elsif missing_default_forged_as_empty_string?(default)
23
+ nil
24
+ else
25
+ super
26
+ end
27
+ end
28
+
29
+ def has_default?
30
+ return false if sql_type =~ /blob/i || type == :text #mysql forbids defaults on blob and text columns
31
+ super
32
+ end
33
+
34
+ # Must return the relevant concrete adapter
35
+ def adapter
36
+ raise NotImplementedError
37
+ end
38
+
39
+ def case_sensitive?
40
+ collation && !collation.match(/_ci$/)
41
+ end
42
+
43
+ private
44
+
45
+ def simplified_type(field_type)
46
+ return :boolean if adapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
47
+
48
+ case field_type
49
+ when /enum/i, /set/i then :string
50
+ when /year/i then :integer
51
+ when /bit/i then :binary
52
+ else
53
+ super
54
+ end
55
+ end
56
+
57
+ def extract_limit(sql_type)
58
+ case sql_type
59
+ when /blob|text/i
60
+ case sql_type
61
+ when /tiny/i
62
+ 255
63
+ when /medium/i
64
+ 16777215
65
+ when /long/i
66
+ 2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
67
+ else
68
+ super # we could return 65535 here, but we leave it undecorated by default
69
+ end
70
+ when /^bigint/i; 8
71
+ when /^int/i; 4
72
+ when /^mediumint/i; 3
73
+ when /^smallint/i; 2
74
+ when /^tinyint/i; 1
75
+ when /^enum\((.+)\)/i
76
+ $1.split(',').map{|enum| enum.strip.length - 2}.max
77
+ else
78
+ super
79
+ end
80
+ end
81
+
82
+ # MySQL misreports NOT NULL column default when none is given.
83
+ # We can't detect this for columns which may have a legitimate ''
84
+ # default (string) but we can for others (integer, datetime, boolean,
85
+ # and the rest).
86
+ #
87
+ # Test whether the column has default '', is not null, and is not
88
+ # a type allowing default ''.
89
+ def missing_default_forged_as_empty_string?(default)
90
+ type != :string && !null && default == ''
91
+ end
92
+ end
93
+
94
+ ##
95
+ # :singleton-method:
96
+ # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt>
97
+ # as boolean. If you wish to disable this emulation (which was the default
98
+ # behavior in versions 0.13.1 and earlier) you can add the following line
99
+ # to your application.rb file:
100
+ #
101
+ # ActiveRecord::ConnectionAdapters::Mysql[2]Adapter.emulate_booleans = false
102
+ class_attribute :emulate_booleans
103
+ self.emulate_booleans = true
104
+
105
+ LOST_CONNECTION_ERROR_MESSAGES = [
106
+ "Server shutdown in progress",
107
+ "Broken pipe",
108
+ "Lost connection to MySQL server during query",
109
+ "MySQL server has gone away" ]
110
+
111
+ QUOTED_TRUE, QUOTED_FALSE = '1', '0'
112
+
113
+ NATIVE_DATABASE_TYPES = {
114
+ :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
115
+ :string => { :name => "varchar", :limit => 255 },
116
+ :text => { :name => "text" },
117
+ :integer => { :name => "int", :limit => 4 },
118
+ :float => { :name => "float" },
119
+ :decimal => { :name => "decimal" },
120
+ :datetime => { :name => "datetime" },
121
+ :timestamp => { :name => "datetime" },
122
+ :time => { :name => "time" },
123
+ :date => { :name => "date" },
124
+ :binary => { :name => "blob" },
125
+ :boolean => { :name => "tinyint", :limit => 1 }
126
+ }
127
+
128
+ class BindSubstitution < Arel::Visitors::MySQL # :nodoc:
129
+ include Arel::Visitors::BindVisitor
130
+ end
131
+
132
+ # FIXME: Make the first parameter more similar for the two adapters
133
+ def initialize(connection, logger, connection_options, config)
134
+ super(connection, logger)
135
+ @connection_options, @config = connection_options, config
136
+ @quoted_column_names, @quoted_table_names = {}, {}
137
+
138
+ if config.fetch(:prepared_statements) { true }
139
+ @visitor = Arel::Visitors::MySQL.new self
140
+ else
141
+ @visitor = BindSubstitution.new self
142
+ end
143
+ end
144
+
145
+ def adapter_name #:nodoc:
146
+ self.class::ADAPTER_NAME
147
+ end
148
+
149
+ # Returns true, since this connection adapter supports migrations.
150
+ def supports_migrations?
151
+ true
152
+ end
153
+
154
+ def supports_primary_key?
155
+ true
156
+ end
157
+
158
+ # Returns true, since this connection adapter supports savepoints.
159
+ def supports_savepoints?
160
+ true
161
+ end
162
+
163
+ def supports_bulk_alter? #:nodoc:
164
+ true
165
+ end
166
+
167
+ # Technically MySQL allows to create indexes with the sort order syntax
168
+ # but at the moment (5.5) it doesn't yet implement them
169
+ def supports_index_sort_order?
170
+ true
171
+ end
172
+
173
+ def native_database_types
174
+ NATIVE_DATABASE_TYPES
175
+ end
176
+
177
+ # HELPER METHODS ===========================================
178
+
179
+ # The two drivers have slightly different ways of yielding hashes of results, so
180
+ # this method must be implemented to provide a uniform interface.
181
+ def each_hash(result) # :nodoc:
182
+ raise NotImplementedError
183
+ end
184
+
185
+ # Overridden by the adapters to instantiate their specific Column type.
186
+ def new_column(field, default, type, null, collation) # :nodoc:
187
+ Column.new(field, default, type, null, collation)
188
+ end
189
+
190
+ # Must return the Mysql error number from the exception, if the exception has an
191
+ # error number.
192
+ def error_number(exception) # :nodoc:
193
+ raise NotImplementedError
194
+ end
195
+
196
+ # QUOTING ==================================================
197
+
198
+ def quote(value, column = nil)
199
+ if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
200
+ s = column.class.string_to_binary(value).unpack("H*")[0]
201
+ "x'#{s}'"
202
+ else
203
+ super
204
+ end
205
+ end
206
+
207
+ def quote_column_name(name) #:nodoc:
208
+ @quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`"
209
+ end
210
+
211
+ def quote_table_name(name) #:nodoc:
212
+ @quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
213
+ end
214
+
215
+ def quoted_true
216
+ QUOTED_TRUE
217
+ end
218
+
219
+ def quoted_false
220
+ QUOTED_FALSE
221
+ end
222
+
223
+ # REFERENTIAL INTEGRITY ====================================
224
+
225
+ def disable_referential_integrity(&block) #:nodoc:
226
+ old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
227
+
228
+ begin
229
+ update("SET FOREIGN_KEY_CHECKS = 0")
230
+ yield
231
+ ensure
232
+ update("SET FOREIGN_KEY_CHECKS = #{old}")
233
+ end
234
+ end
235
+
236
+ # DATABASE STATEMENTS ======================================
237
+
238
+ # Executes the SQL statement in the context of this connection.
239
+ def execute(sql, name = nil)
240
+ if name == :skip_logging
241
+ @connection.query(sql)
242
+ else
243
+ log(sql, name) { @connection.query(sql) }
244
+ end
245
+ rescue ActiveRecord::StatementInvalid => exception
246
+ if exception.message.split(":").first =~ /Packets out of order/
247
+ 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."
248
+ else
249
+ raise
250
+ end
251
+ end
252
+
253
+ # MysqlAdapter has to free a result after using it, so we use this method to write
254
+ # stuff in a abstract way without concerning ourselves about whether it needs to be
255
+ # explicitly freed or not.
256
+ def execute_and_free(sql, name = nil) #:nodoc:
257
+ yield execute(sql, name)
258
+ end
259
+
260
+ def update_sql(sql, name = nil) #:nodoc:
261
+ super
262
+ @connection.affected_rows
263
+ end
264
+
265
+ def begin_db_transaction
266
+ execute "BEGIN"
267
+ rescue Exception
268
+ # Transactions aren't supported
269
+ end
270
+
271
+ def commit_db_transaction #:nodoc:
272
+ execute "COMMIT"
273
+ rescue Exception
274
+ # Transactions aren't supported
275
+ end
276
+
277
+ def rollback_db_transaction #:nodoc:
278
+ execute "ROLLBACK"
279
+ rescue Exception
280
+ # Transactions aren't supported
281
+ end
282
+
283
+ def create_savepoint
284
+ execute("SAVEPOINT #{current_savepoint_name}")
285
+ end
286
+
287
+ def rollback_to_savepoint
288
+ execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
289
+ end
290
+
291
+ def release_savepoint
292
+ execute("RELEASE SAVEPOINT #{current_savepoint_name}")
293
+ end
294
+
295
+ # In the simple case, MySQL allows us to place JOINs directly into the UPDATE
296
+ # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
297
+ # these, we must use a subquery. However, MySQL is too stupid to create a
298
+ # temporary table for this automatically, so we have to give it some prompting
299
+ # in the form of a subsubquery. Ugh!
300
+ def join_to_update(update, select) #:nodoc:
301
+ if select.limit || select.offset || select.orders.any?
302
+ subsubselect = select.clone
303
+ subsubselect.projections = [update.key]
304
+
305
+ subselect = Arel::SelectManager.new(select.engine)
306
+ subselect.project Arel.sql(update.key.name)
307
+ subselect.from subsubselect.as('__active_record_temp')
308
+
309
+ update.where update.key.in(subselect)
310
+ else
311
+ update.table select.source
312
+ update.wheres = select.constraints
313
+ end
314
+ end
315
+
316
+ # SCHEMA STATEMENTS ========================================
317
+
318
+ def structure_dump #:nodoc:
319
+ if supports_views?
320
+ sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
321
+ else
322
+ sql = "SHOW TABLES"
323
+ end
324
+
325
+ select_all(sql).map { |table|
326
+ table.delete('Table_type')
327
+ sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}"
328
+ exec_query(sql).first['Create Table'] + ";\n\n"
329
+ }.join
330
+ end
331
+
332
+ # Drops the database specified on the +name+ attribute
333
+ # and creates it again using the provided +options+.
334
+ def recreate_database(name, options = {})
335
+ drop_database(name)
336
+ create_database(name, options)
337
+ end
338
+
339
+ # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
340
+ # Charset defaults to utf8.
341
+ #
342
+ # Example:
343
+ # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
344
+ # create_database 'matt_development'
345
+ # create_database 'matt_development', :charset => :big5
346
+ def create_database(name, options = {})
347
+ if options[:collation]
348
+ execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
349
+ else
350
+ execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
351
+ end
352
+ end
353
+
354
+ # Drops a MySQL database.
355
+ #
356
+ # Example:
357
+ # drop_database('sebastian_development')
358
+ def drop_database(name) #:nodoc:
359
+ execute "DROP DATABASE IF EXISTS `#{name}`"
360
+ end
361
+
362
+ def current_database
363
+ select_value 'SELECT DATABASE() as db'
364
+ end
365
+
366
+ # Returns the database character set.
367
+ def charset
368
+ show_variable 'character_set_database'
369
+ end
370
+
371
+ # Returns the database collation strategy.
372
+ def collation
373
+ show_variable 'collation_database'
374
+ end
375
+
376
+ def tables(name = nil, database = nil, like = nil) #:nodoc:
377
+ sql = "SHOW TABLES "
378
+ sql << "IN #{quote_table_name(database)} " if database
379
+ sql << "LIKE #{quote(like)}" if like
380
+
381
+ execute_and_free(sql, 'SCHEMA') do |result|
382
+ result.collect { |field| field.first }
383
+ end
384
+ end
385
+
386
+ def table_exists?(name)
387
+ return false unless name
388
+ return true if tables(nil, nil, name).any?
389
+
390
+ name = name.to_s
391
+ schema, table = name.split('.', 2)
392
+
393
+ unless table # A table was provided without a schema
394
+ table = schema
395
+ schema = nil
396
+ end
397
+
398
+ tables(nil, schema, table).any?
399
+ end
400
+
401
+ # Returns an array of indexes for the given table.
402
+ def indexes(table_name, name = nil) #:nodoc:
403
+ indexes = []
404
+ current_index = nil
405
+ execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", 'SCHEMA') do |result|
406
+ each_hash(result) do |row|
407
+ if current_index != row[:Key_name]
408
+ next if row[:Key_name] == 'PRIMARY' # skip the primary key
409
+ current_index = row[:Key_name]
410
+ indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [])
411
+ end
412
+
413
+ indexes.last.columns << row[:Column_name]
414
+ indexes.last.lengths << row[:Sub_part]
415
+ end
416
+ end
417
+
418
+ indexes
419
+ end
420
+
421
+ # Returns an array of +Column+ objects for the table specified by +table_name+.
422
+ def columns(table_name, name = nil)#:nodoc:
423
+ sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
424
+ execute_and_free(sql, 'SCHEMA') do |result|
425
+ each_hash(result).map do |field|
426
+ new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES", field[:Collation])
427
+ end
428
+ end
429
+ end
430
+
431
+ def create_table(table_name, options = {}) #:nodoc:
432
+ super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
433
+ end
434
+
435
+ def bulk_change_table(table_name, operations) #:nodoc:
436
+ sqls = operations.map do |command, args|
437
+ table, arguments = args.shift, args
438
+ method = :"#{command}_sql"
439
+
440
+ if respond_to?(method, true)
441
+ send(method, table, *arguments)
442
+ else
443
+ raise "Unknown method called : #{method}(#{arguments.inspect})"
444
+ end
445
+ end.flatten.join(", ")
446
+
447
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
448
+ end
449
+
450
+ # Renames a table.
451
+ #
452
+ # Example:
453
+ # rename_table('octopuses', 'octopi')
454
+ def rename_table(table_name, new_name)
455
+ execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
456
+ end
457
+
458
+ def add_column(table_name, column_name, type, options = {})
459
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}")
460
+ end
461
+
462
+ def change_column_default(table_name, column_name, default)
463
+ column = column_for(table_name, column_name)
464
+ change_column table_name, column_name, column.sql_type, :default => default
465
+ end
466
+
467
+ def change_column_null(table_name, column_name, null, default = nil)
468
+ column = column_for(table_name, column_name)
469
+
470
+ unless null || default.nil?
471
+ execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
472
+ end
473
+
474
+ change_column table_name, column_name, column.sql_type, :null => null
475
+ end
476
+
477
+ def change_column(table_name, column_name, type, options = {}) #:nodoc:
478
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}")
479
+ end
480
+
481
+ def rename_column(table_name, column_name, new_column_name) #:nodoc:
482
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
483
+ end
484
+
485
+ # Maps logical Rails types to MySQL-specific data types.
486
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
487
+ case type.to_s
488
+ when 'integer'
489
+ case limit
490
+ when 1; 'tinyint'
491
+ when 2; 'smallint'
492
+ when 3; 'mediumint'
493
+ when nil, 4, 11; 'int(11)' # compatibility with MySQL default
494
+ when 5..8; 'bigint'
495
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}")
496
+ end
497
+ when 'text'
498
+ case limit
499
+ when 0..0xff; 'tinytext'
500
+ when nil, 0x100..0xffff; 'text'
501
+ when 0x10000..0xffffff; 'mediumtext'
502
+ when 0x1000000..0xffffffff; 'longtext'
503
+ else raise(ActiveRecordError, "No text type has character length #{limit}")
504
+ end
505
+ else
506
+ super
507
+ end
508
+ end
509
+
510
+ def add_column_position!(sql, options)
511
+ if options[:first]
512
+ sql << " FIRST"
513
+ elsif options[:after]
514
+ sql << " AFTER #{quote_column_name(options[:after])}"
515
+ end
516
+ end
517
+
518
+ # SHOW VARIABLES LIKE 'name'
519
+ def show_variable(name)
520
+ variables = select_all("SHOW VARIABLES LIKE '#{name}'")
521
+ variables.first['Value'] unless variables.empty?
522
+ end
523
+
524
+ # Returns a table's primary key and belonging sequence.
525
+ def pk_and_sequence_for(table)
526
+ execute_and_free("SHOW CREATE TABLE #{quote_table_name(table)}", 'SCHEMA') do |result|
527
+ create_table = each_hash(result).first[:"Create Table"]
528
+ if create_table.to_s =~ /PRIMARY KEY\s+(?:USING\s+\w+\s+)?\((.+)\)/
529
+ keys = $1.split(",").map { |key| key.gsub(/[`"]/, "") }
530
+ keys.length == 1 ? [keys.first, nil] : nil
531
+ else
532
+ nil
533
+ end
534
+ end
535
+ end
536
+
537
+ # Returns just a table's primary key
538
+ def primary_key(table)
539
+ pk_and_sequence = pk_and_sequence_for(table)
540
+ pk_and_sequence && pk_and_sequence.first
541
+ end
542
+
543
+ def case_sensitive_modifier(node)
544
+ Arel::Nodes::Bin.new(node)
545
+ end
546
+
547
+ def case_insensitive_comparison(table, attribute, column, value)
548
+ if column.case_sensitive?
549
+ super
550
+ else
551
+ table[attribute].eq(value)
552
+ end
553
+ end
554
+
555
+ def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
556
+ where_sql
557
+ end
558
+
559
+ protected
560
+
561
+ def add_index_length(option_strings, column_names, options = {})
562
+ if options.is_a?(Hash) && length = options[:length]
563
+ case length
564
+ when Hash
565
+ column_names.each {|name| option_strings[name] += "(#{length[name]})" if length.has_key?(name) && length[name].present?}
566
+ when Fixnum
567
+ column_names.each {|name| option_strings[name] += "(#{length})"}
568
+ end
569
+ end
570
+
571
+ return option_strings
572
+ end
573
+
574
+ def quoted_columns_for_index(column_names, options = {})
575
+ option_strings = Hash[column_names.map {|name| [name, '']}]
576
+
577
+ # add index length
578
+ option_strings = add_index_length(option_strings, column_names, options)
579
+
580
+ # add index sort order
581
+ option_strings = add_index_sort_order(option_strings, column_names, options)
582
+
583
+ column_names.map {|name| quote_column_name(name) + option_strings[name]}
584
+ end
585
+
586
+ def translate_exception(exception, message)
587
+ case error_number(exception)
588
+ when 1062
589
+ RecordNotUnique.new(message, exception)
590
+ when 1452
591
+ InvalidForeignKey.new(message, exception)
592
+ else
593
+ super
594
+ end
595
+ end
596
+
597
+ def add_column_sql(table_name, column_name, type, options = {})
598
+ add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
599
+ add_column_options!(add_column_sql, options)
600
+ add_column_position!(add_column_sql, options)
601
+ add_column_sql
602
+ end
603
+
604
+ def change_column_sql(table_name, column_name, type, options = {})
605
+ column = column_for(table_name, column_name)
606
+
607
+ unless options_include_default?(options)
608
+ options[:default] = column.default
609
+ end
610
+
611
+ unless options.has_key?(:null)
612
+ options[:null] = column.null
613
+ end
614
+
615
+ change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
616
+ add_column_options!(change_column_sql, options)
617
+ add_column_position!(change_column_sql, options)
618
+ change_column_sql
619
+ end
620
+
621
+ def rename_column_sql(table_name, column_name, new_column_name)
622
+ options = {}
623
+
624
+ if column = columns(table_name).find { |c| c.name == column_name.to_s }
625
+ options[:default] = column.default
626
+ options[:null] = column.null
627
+ else
628
+ raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
629
+ end
630
+
631
+ current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
632
+ rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
633
+ add_column_options!(rename_column_sql, options)
634
+ rename_column_sql
635
+ end
636
+
637
+ def remove_column_sql(table_name, *column_names)
638
+ columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" }
639
+ end
640
+ alias :remove_columns_sql :remove_column
641
+
642
+ def add_index_sql(table_name, column_name, options = {})
643
+ index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
644
+ "ADD #{index_type} INDEX #{index_name} (#{index_columns})"
645
+ end
646
+
647
+ def remove_index_sql(table_name, options = {})
648
+ index_name = index_name_for_remove(table_name, options)
649
+ "DROP INDEX #{index_name}"
650
+ end
651
+
652
+ def add_timestamps_sql(table_name)
653
+ [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)]
654
+ end
655
+
656
+ def remove_timestamps_sql(table_name)
657
+ [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
658
+ end
659
+
660
+ private
661
+
662
+ def supports_views?
663
+ version[0] >= 5
664
+ end
665
+
666
+ def column_for(table_name, column_name)
667
+ unless column = columns(table_name).find { |c| c.name == column_name.to_s }
668
+ raise "No such column: #{table_name}.#{column_name}"
669
+ end
670
+ column
671
+ end
672
+ end
673
+ end
674
+ end