diff options
Diffstat (limited to 'activerecord')
17 files changed, 203 insertions, 48 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 780c34147d..6312960819 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,14 @@ +* MySQL: `:charset` and `:collation` support for string and text columns. + + Example: + + create_table :foos do |t| + t.string :string_utf8_bin, charset: 'utf8', collation: 'utf8_bin' + t.text :text_ascii, charset: 'ascii' + end + + *Ryuta Kamizono* + * Foreign key related methods in the migration DSL respect `ActiveRecord::Base.pluralize_table_names = false`. diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 88531205a1..6caadb4ce8 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -370,6 +370,8 @@ module ActiveRecord replace_common_records_in_memory(other_array, original_target) if other_array != original_target transaction { replace_records(other_array, original_target) } + else + other_array end end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 4897ec44e9..29e8a0edc1 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -135,7 +135,7 @@ module ActiveRecord if scope.klass.primary_key count = scope.destroy_all.length else - scope.each(&:_run_destroy_callbacks) + scope.each { |record| record.run_callbacks :destroy } arel = scope.arel diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index f44e5af5de..2fcba8e309 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -289,25 +289,24 @@ module ActiveRecord end def destroy #:nodoc: - _run_destroy_callbacks { super } + run_callbacks(:destroy) { super } end def touch(*) #:nodoc: - _run_touch_callbacks { super } + run_callbacks(:touch) { super } end private - def create_or_update(*) #:nodoc: - _run_save_callbacks { super } + run_callbacks(:save) { super } end def _create_record #:nodoc: - _run_create_callbacks { super } + run_callbacks(:create) { super } end def _update_record(*) #:nodoc: - _run_update_callbacks { super } + run_callbacks(:update) { super } end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index d99dc9a5db..8c50f3d1a3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -358,7 +358,7 @@ module ActiveRecord synchronize do owner = conn.owner - conn._run_checkin_callbacks do + conn.run_callbacks :checkin do conn.expire end @@ -449,7 +449,7 @@ module ActiveRecord end def checkout_and_verify(c) - c._run_checkout_callbacks do + c.run_callbacks :checkout do c.verify! end c diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index c1b4c936be..ab1b098e53 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -377,6 +377,9 @@ module ActiveRecord # [<tt>:force</tt>] # Set to +:cascade+ to drop dependent objects as well. # Defaults to false. + # [<tt>:if_exists</tt>] + # Set to +true+ to only drop the table if it exists. + # Defaults to false. # # Although this command ignores most +options+ and the block if one is given, # it can be helpful to provide these in a migration's +change+ method so it can be reverted. diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 51c41cd588..76aee452ca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -13,6 +13,10 @@ module ActiveRecord end end + class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition + attr_accessor :charset, :collation + end + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods @@ -23,8 +27,16 @@ module ActiveRecord column.type = :integer column.auto_increment = true end + column.charset = options[:charset] + column.collation = options[:collation] column end + + private + + def create_column_definition(name, type) + ColumnDefinition.new(name, type) + end end class Table < ActiveRecord::ConnectionAdapters::Table @@ -60,6 +72,23 @@ module ActiveRecord add_column_position!(change_column_sql, column_options(o.column)) end + def column_options(o) + column_options = super + column_options[:charset] = o.charset + column_options[:collation] = o.collation + column_options + end + + def add_column_options!(sql, options) + if options[:charset] + sql << " CHARACTER SET #{options[:charset]}" + end + if options[:collation] + sql << " COLLATE #{options[:collation]}" + end + super + end + def add_column_position!(sql, options) if options[:first] sql << " FIRST" @@ -99,9 +128,18 @@ module ActiveRecord spec = super spec.delete(:precision) if /time/ === column.sql_type && column.precision == 0 spec.delete(:limit) if :boolean === column.type + if column.collation && table_name = column.instance_variable_get(:@table_name) + @collation_cache ||= {} + @collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"] + spec[:collation] = column.collation.inspect if column.collation != @collation_cache[table_name] + end spec end + def migration_keys + super + [:collation] + end + class Column < ConnectionAdapters::Column # :nodoc: delegate :strict, :collation, :extra, to: :sql_type_metadata, allow_nil: true @@ -568,11 +606,15 @@ module ActiveRecord # Set to +:cascade+ to drop dependent objects as well. # Defaults to false. # [<tt>:if_exists</tt>] - # Set to +true+ to make drop table command fail safe when table does not exists. + # Set to +true+ to only drop the table if it exists. # Defaults to false. # [<tt>:temporary</tt>] # Set to +true+ to drop temporary table. # Defaults to false. + # + # Although this command ignores most +options+ and the block if one is given, + # it can be helpful to provide these in a migration's +change+ method so it can be reverted. + # In that case, +options+ and the block will be used by create_table. def drop_table(table_name, options = {}) execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index a67127bd71..f4dda5154e 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -28,6 +28,7 @@ module ActiveRecord @null = null @default = default @default_function = default_function + @table_name = nil end def has_default? diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 652ae1ed63..168180cfd3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -87,15 +87,7 @@ module ActiveRecord SQL end - # Drops a table from the database. - # - # [<tt>:force</tt>] - # Set to +:cascade+ to drop dependent objects as well. - # Defaults to false. - # [<tt>:if_exists</tt>] - # Set to +true+ to make drop table command fail safe when table does not exists. - # Defaults to false. - def drop_table(table_name, options = {}) + def drop_table(table_name, options = {}) # :nodoc: execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 9b7cba08de..1ad910c4bc 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -302,7 +302,7 @@ module ActiveRecord assign_attributes(attributes) if attributes yield self if block_given? - _run_initialize_callbacks + run_callbacks :initialize end # Initialize an empty model object from +coder+. +coder+ should be @@ -329,8 +329,8 @@ module ActiveRecord self.class.define_attribute_methods - _run_find_callbacks - _run_initialize_callbacks + run_callbacks :find + run_callbacks :initialize self end @@ -366,7 +366,7 @@ module ActiveRecord @attributes = @attributes.dup @attributes.reset(self.class.primary_key) - _run_initialize_callbacks + run_callbacks(:initialize) @new_record = true @destroyed = false diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index da95920571..eaeaf0321b 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -105,7 +105,10 @@ HEADER end def table(table, stream) - columns = @connection.columns(table) + columns = @connection.columns(table).map do |column| + column.instance_variable_set(:@table_name, table) + column + end begin tbl = StringIO.new diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 2293d1b258..311dacb449 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -319,8 +319,8 @@ module ActiveRecord end def before_committed! # :nodoc: - _run_before_commit_without_transaction_enrollment_callbacks - _run_before_commit_callbacks + run_callbacks :before_commit_without_transaction_enrollment + run_callbacks :before_commit end # Call the +after_commit+ callbacks. @@ -329,8 +329,8 @@ module ActiveRecord # but call it after the commit of a destroyed object. def committed!(should_run_callbacks: true) #:nodoc: if should_run_callbacks && destroyed? || persisted? - _run_commit_without_transaction_enrollment_callbacks - _run_commit_callbacks + run_callbacks :commit_without_transaction_enrollment + run_callbacks :commit end ensure force_clear_transaction_record_state @@ -340,8 +340,8 @@ module ActiveRecord # state should be rolled back to the beginning or just to the last savepoint. def rolledback!(force_restore_state: false, should_run_callbacks: true) #:nodoc: if should_run_callbacks - _run_rollback_without_transaction_enrollment_callbacks - _run_rollback_callbacks + run_callbacks :rollback + run_callbacks :rollback_without_transaction_enrollment end ensure restore_transaction_record_state(force_restore_state) diff --git a/activerecord/test/cases/adapters/mysql/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql/charset_collation_test.rb new file mode 100644 index 0000000000..71a3756a90 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/charset_collation_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class CharsetCollationTest < ActiveRecord::TestCase + include SchemaDumpingHelper + self.use_transactional_fixtures = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :charset_collations, force: true do |t| + t.string :string_ascii_bin, charset: 'ascii', collation: 'ascii_bin' + t.text :text_ucs2_unicode_ci, charset: 'ucs2', collation: 'ucs2_unicode_ci' + end + end + + teardown do + @connection.drop_table :charset_collations, if_exists: true + end + + test "string column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' } + assert_equal :string, column.type + assert_equal 'ascii_bin', column.collation + end + + test "text column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'text_ucs2_unicode_ci' } + assert_equal :text, column.type + assert_equal 'ucs2_unicode_ci', column.collation + end + + test "add column with charset and collation" do + @connection.add_column :charset_collations, :title, :string, charset: 'utf8', collation: 'utf8_bin' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'utf8_bin', column.collation + end + + test "change column with charset and collation" do + @connection.add_column :charset_collations, :description, :string, charset: 'utf8', collation: 'utf8_unicode_ci' + @connection.change_column :charset_collations, :description, :text, charset: 'utf8', collation: 'utf8_general_ci' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'utf8_general_ci', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("charset_collations") + assert_match %r{t.string\s+"string_ascii_bin",\s+limit: 255,\s+collation: "ascii_bin"$}, output + assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output + end +end diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb new file mode 100644 index 0000000000..71a3756a90 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class CharsetCollationTest < ActiveRecord::TestCase + include SchemaDumpingHelper + self.use_transactional_fixtures = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :charset_collations, force: true do |t| + t.string :string_ascii_bin, charset: 'ascii', collation: 'ascii_bin' + t.text :text_ucs2_unicode_ci, charset: 'ucs2', collation: 'ucs2_unicode_ci' + end + end + + teardown do + @connection.drop_table :charset_collations, if_exists: true + end + + test "string column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' } + assert_equal :string, column.type + assert_equal 'ascii_bin', column.collation + end + + test "text column with charset and collation" do + column = @connection.columns(:charset_collations).find { |c| c.name == 'text_ucs2_unicode_ci' } + assert_equal :text, column.type + assert_equal 'ucs2_unicode_ci', column.collation + end + + test "add column with charset and collation" do + @connection.add_column :charset_collations, :title, :string, charset: 'utf8', collation: 'utf8_bin' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'utf8_bin', column.collation + end + + test "change column with charset and collation" do + @connection.add_column :charset_collations, :description, :string, charset: 'utf8', collation: 'utf8_unicode_ci' + @connection.change_column :charset_collations, :description, :text, charset: 'utf8', collation: 'utf8_general_ci' + + column = @connection.columns(:charset_collations).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'utf8_general_ci', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("charset_collations") + assert_match %r{t.string\s+"string_ascii_bin",\s+limit: 255,\s+collation: "ascii_bin"$}, output + assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output + end +end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 290b2a0d6b..171cfbde44 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1503,6 +1503,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_queries(0, ignore_none: true) do firm.clients = [] end + + assert_equal [], firm.send('clients=', []) end def test_transactions_when_replacing_on_persisted diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index d6fd0c4ab0..52d3290c84 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -24,6 +24,11 @@ ActiveRecord::Schema.define do add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' + create_table :collation_tests, id: false, force: true do |t| + t.string :string_cs_column, limit: 1, collation: 'utf8_bin' + t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci' + end + ActiveRecord::Base.connection.execute <<-SQL DROP PROCEDURE IF EXISTS ten; SQL @@ -35,15 +40,6 @@ BEGIN END SQL - ActiveRecord::Base.connection.drop_table "collation_tests", if_exists: true - - ActiveRecord::Base.connection.execute <<-SQL -CREATE TABLE collation_tests ( - string_cs_column VARCHAR(1) COLLATE utf8_bin, - string_ci_column VARCHAR(1) COLLATE utf8_general_ci -) CHARACTER SET utf8 COLLATE utf8_general_ci -SQL - ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true ActiveRecord::Base.connection.execute <<-SQL diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb index b5378341b5..90f5a60d7b 100644 --- a/activerecord/test/schema/mysql_specific_schema.rb +++ b/activerecord/test/schema/mysql_specific_schema.rb @@ -24,6 +24,11 @@ ActiveRecord::Schema.define do add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' + create_table :collation_tests, id: false, force: true do |t| + t.string :string_cs_column, limit: 1, collation: 'utf8_bin' + t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci' + end + ActiveRecord::Base.connection.execute <<-SQL DROP PROCEDURE IF EXISTS ten; SQL @@ -46,15 +51,6 @@ BEGIN END SQL - ActiveRecord::Base.connection.drop_table "collation_tests", if_exists: true - - ActiveRecord::Base.connection.execute <<-SQL -CREATE TABLE collation_tests ( - string_cs_column VARCHAR(1) COLLATE utf8_bin, - string_ci_column VARCHAR(1) COLLATE utf8_general_ci -) CHARACTER SET utf8 COLLATE utf8_general_ci -SQL - ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true ActiveRecord::Base.connection.execute <<-SQL |