diff options
Diffstat (limited to 'activerecord/test/cases/adapters/mysql2')
24 files changed, 965 insertions, 582 deletions
diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 99f97c7914..88c2ac5d0a 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -1,13 +1,16 @@ +# frozen_string_literal: true + require "cases/helper" -require 'support/connection_helper' +require "support/connection_helper" class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase include ConnectionHelper def setup + ActiveRecord::Base.connection.send(:default_row_format) ActiveRecord::Base.connection.singleton_class.class_eval do alias_method :execute_without_stub, :execute - def execute(sql, name = nil) return sql end + def execute(sql, name = nil) sql end end end @@ -21,60 +24,63 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase def (ActiveRecord::Base.connection).index_name_exists?(*); false; end expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) " - assert_equal expected, add_index(:people, :last_name, :length => nil) + assert_equal expected, add_index(:people, :last_name, length: nil) expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10)) " - assert_equal expected, add_index(:people, :last_name, :length => 10) + assert_equal expected, add_index(:people, :last_name, length: 10) expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15)) " - assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15) + assert_equal expected, add_index(:people, [:last_name, :first_name], length: 15) + assert_equal expected, add_index(:people, ["last_name", "first_name"], length: 15) expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`) " - assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15}) + assert_equal expected, add_index(:people, [:last_name, :first_name], length: { last_name: 15 }) + assert_equal expected, add_index(:people, ["last_name", "first_name"], length: { last_name: 15 }) expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10)) " - assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15, :first_name => 10}) + assert_equal expected, add_index(:people, [:last_name, :first_name], length: { last_name: 15, first_name: 10 }) + assert_equal expected, add_index(:people, ["last_name", :first_name], length: { last_name: 15, "first_name" => 10 }) %w(SPATIAL FULLTEXT UNIQUE).each do |type| expected = "CREATE #{type} INDEX `index_people_on_last_name` ON `people` (`last_name`) " - assert_equal expected, add_index(:people, :last_name, :type => type) + assert_equal expected, add_index(:people, :last_name, type: type) end %w(btree hash).each do |using| expected = "CREATE INDEX `index_people_on_last_name` USING #{using} ON `people` (`last_name`) " - assert_equal expected, add_index(:people, :last_name, :using => using) + assert_equal expected, add_index(:people, :last_name, using: using) end expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) " - assert_equal expected, add_index(:people, :last_name, :length => 10, :using => :btree) + assert_equal expected, add_index(:people, :last_name, length: 10, using: :btree) expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) ALGORITHM = COPY" - assert_equal expected, add_index(:people, :last_name, :length => 10, using: :btree, algorithm: :copy) + assert_equal expected, add_index(:people, :last_name, length: 10, using: :btree, algorithm: :copy) assert_raise ArgumentError do add_index(:people, :last_name, algorithm: :coyp) end expected = "CREATE INDEX `index_people_on_last_name_and_first_name` USING btree ON `people` (`last_name`(15), `first_name`(15)) " - assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree) + assert_equal expected, add_index(:people, [:last_name, :first_name], length: 15, using: :btree) end def test_index_in_create def (ActiveRecord::Base.connection).data_source_exists?(*); false; end %w(SPATIAL FULLTEXT UNIQUE).each do |type| - expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`) ) ENGINE=InnoDB" + expected = /\ACREATE TABLE `people` \(#{type} INDEX `index_people_on_last_name` \(`last_name`\)\)/ actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| t.index :last_name, type: type end - assert_equal expected, actual + assert_match expected, actual end - expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)) ) ENGINE=InnoDB" + expected = /\ACREATE TABLE `people` \( INDEX `index_people_on_last_name` USING btree \(`last_name`\(10\)\)\)/ actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| t.index :last_name, length: 10, using: :btree end - assert_equal expected, actual + assert_match expected, actual end def test_index_in_bulk_change @@ -89,8 +95,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase assert_equal expected, actual end - expected = "ALTER TABLE `peaple` ADD INDEX `index_peaple_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" - actual = ActiveRecord::Base.connection.change_table(:peaple, bulk: true) do |t| + expected = "ALTER TABLE `people` ADD INDEX `index_people_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" + actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| t.index :last_name, length: 10, using: :btree, algorithm: :copy end assert_equal expected, actual @@ -101,14 +107,20 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_create_mysql_database_with_encoding - assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt) - assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'}) - assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci}) + if row_format_dynamic_by_default? + assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8mb4`", create_database(:matt) + else + error = assert_raises(RuntimeError) { create_database(:matt) } + expected = "Configure a supported :charset and ensure innodb_large_prefix is enabled to support indexes on varchar(255) string columns." + assert_equal expected, error.message + end + assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, charset: "latin1") + assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT COLLATE `utf8mb4_bin`", create_database(:matt_aimonetti, collation: "utf8mb4_bin") end def test_recreate_mysql_database_with_encoding - create_database(:luca, {:charset => 'latin1'}) - assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'}) + create_database(:luca, charset: "latin1") + assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, charset: "latin1") end def test_add_column @@ -116,51 +128,51 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_add_column_with_limit - assert_equal "ALTER TABLE `people` ADD `key` varchar(32)", add_column(:people, :key, :string, :limit => 32) + assert_equal "ALTER TABLE `people` ADD `key` varchar(32)", add_column(:people, :key, :string, limit: 32) end def test_drop_table_with_specific_database - assert_equal "DROP TABLE `otherdb`.`people`", drop_table('otherdb.people') + assert_equal "DROP TABLE `otherdb`.`people`", drop_table("otherdb.people") end def test_add_timestamps with_real_execute do - begin - ActiveRecord::Base.connection.create_table :delete_me - ActiveRecord::Base.connection.add_timestamps :delete_me, null: true - assert column_present?('delete_me', 'updated_at', 'datetime') - assert column_present?('delete_me', 'created_at', 'datetime') - ensure - ActiveRecord::Base.connection.drop_table :delete_me rescue nil - end + ActiveRecord::Base.connection.create_table :delete_me + ActiveRecord::Base.connection.add_timestamps :delete_me, null: true + assert column_exists?("delete_me", "updated_at", "datetime") + assert column_exists?("delete_me", "created_at", "datetime") + ensure + ActiveRecord::Base.connection.drop_table :delete_me rescue nil end end def test_remove_timestamps with_real_execute do - begin - ActiveRecord::Base.connection.create_table :delete_me do |t| - t.timestamps null: true - end - ActiveRecord::Base.connection.remove_timestamps :delete_me, { null: true } - assert !column_present?('delete_me', 'updated_at', 'datetime') - assert !column_present?('delete_me', 'created_at', 'datetime') - ensure - ActiveRecord::Base.connection.drop_table :delete_me rescue nil + ActiveRecord::Base.connection.create_table :delete_me do |t| + t.timestamps null: true end + ActiveRecord::Base.connection.remove_timestamps :delete_me, null: true + assert_not column_exists?("delete_me", "updated_at", "datetime") + assert_not column_exists?("delete_me", "created_at", "datetime") + ensure + ActiveRecord::Base.connection.drop_table :delete_me rescue nil end end def test_indexes_in_create - ActiveRecord::Base.connection.stubs(:data_source_exists?).with(:temp).returns(false) - ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false) + assert_called_with( + ActiveRecord::Base.connection, + :data_source_exists?, + [:temp], + returns: false + ) do + expected = /\ACREATE TEMPORARY TABLE `temp` \( INDEX `index_temp_on_zip` \(`zip`\)\)(?: ROW_FORMAT=DYNAMIC)? AS SELECT id, name, zip FROM a_really_complicated_query/ + actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t| + t.index :zip + end - expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query" - actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t| - t.index :zip + assert_match expected, actual end - - assert_equal expected, actual end private @@ -182,9 +194,4 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase def method_missing(method_symbol, *arguments) ActiveRecord::Base.connection.send(method_symbol, *arguments) end - - def column_present?(table_name, column_name, type) - results = ActiveRecord::Base.connection.select_all("SHOW FIELDS FROM #{table_name} LIKE '#{column_name}'") - results.first && results.first['Type'] == type - end end diff --git a/activerecord/test/cases/adapters/mysql2/annotate_test.rb b/activerecord/test/cases/adapters/mysql2/annotate_test.rb new file mode 100644 index 0000000000..b512540073 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/annotate_test.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/post" + +class Mysql2AnnotateTest < ActiveRecord::Mysql2TestCase + fixtures :posts + + def test_annotate_wraps_content_in_an_inline_comment + assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/}) do + posts = Post.select(:id).annotate("foo") + assert posts.first + end + end + + def test_annotate_is_sanitized + assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/}) do + posts = Post.select(:id).annotate("*/foo/*") + assert posts.first + end + + assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/}) do + posts = Post.select(:id).annotate("**//foo//**") + assert posts.first + end + + assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/ /\* bar \*/}) do + posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar") + assert posts.first + end + + assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* \+ MAX_EXECUTION_TIME\(1\) \*/}) do + posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)") + assert posts.first + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/auto_increment_test.rb b/activerecord/test/cases/adapters/mysql2/auto_increment_test.rb new file mode 100644 index 0000000000..4c67633946 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/auto_increment_test.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class Mysql2AutoIncrementTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + @connection.drop_table :auto_increments, if_exists: true + end + + def test_auto_increment_without_primary_key + @connection.create_table :auto_increments, id: false, force: true do |t| + t.integer :id, null: false, auto_increment: true + t.index :id + end + output = dump_table_schema("auto_increments") + assert_match(/t\.integer\s+"id",\s+null: false,\s+auto_increment: true$/, output) + end + + def test_auto_increment_with_composite_primary_key + @connection.create_table :auto_increments, primary_key: [:id, :created_at], force: true do |t| + t.integer :id, null: false, auto_increment: true + t.datetime :created_at, null: false + end + output = dump_table_schema("auto_increments") + assert_match(/t\.integer\s+"id",\s+null: false,\s+auto_increment: true$/, output) + end +end diff --git a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb index abdf3dbf5b..825bddfb73 100644 --- a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require "cases/helper" -require 'models/topic' +require "models/topic" module ActiveRecord module ConnectionAdapters @@ -20,7 +22,7 @@ module ActiveRecord def test_create_question_marks str = "foo?bar" - x = Topic.create!(:title => str, :content => str) + x = Topic.create!(title: str, content: str) x.reload assert_equal str, x.title assert_equal str, x.content @@ -39,7 +41,7 @@ module ActiveRecord def test_create_null_bytes str = "foo\0bar" - x = Topic.create!(:title => str, :content => str) + x = Topic.create!(title: str, content: str) x.reload assert_equal str, x.title assert_equal str, x.content diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 8575df9e43..db09b30361 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase @@ -38,28 +40,38 @@ class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase assert_equal :string, string_column.type end - test "test type casting with emulated booleans" do + test "type casting with emulated booleans" do emulate_booleans true boolean = BooleanType.create!(archived: true, published: true) attributes = boolean.reload.attributes_before_type_cast - assert_equal 1, attributes["archived"] assert_equal "1", attributes["published"] + boolean = BooleanType.create!(archived: false, published: false) + attributes = boolean.reload.attributes_before_type_cast + assert_equal 0, attributes["archived"] + assert_equal "0", attributes["published"] + assert_equal 1, @connection.type_cast(true) + assert_equal 0, @connection.type_cast(false) end - test "test type casting without emulated booleans" do + test "type casting without emulated booleans" do emulate_booleans false boolean = BooleanType.create!(archived: true, published: true) attributes = boolean.reload.attributes_before_type_cast - assert_equal 1, attributes["archived"] assert_equal "1", attributes["published"] + boolean = BooleanType.create!(archived: false, published: false) + attributes = boolean.reload.attributes_before_type_cast + assert_equal 0, attributes["archived"] + assert_equal "0", attributes["published"] + assert_equal 1, @connection.type_cast(true) + assert_equal 0, @connection.type_cast(false) end test "with booleans stored as 1 and 0" do @@ -76,11 +88,11 @@ class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase end def boolean_column - BooleanType.columns.find { |c| c.name == 'archived' } + BooleanType.columns.find { |c| c.name == "archived" } end def string_column - BooleanType.columns.find { |c| c.name == 'published' } + BooleanType.columns.find { |c| c.name == "published" } end def emulate_booleans(value) diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb index 9cb05119a2..c32475c683 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase @@ -7,46 +9,46 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase repair_validations(CollationTest) def test_columns_include_collation_different_from_table - assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation - assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation + assert_equal "utf8mb4_bin", CollationTest.columns_hash["string_cs_column"].collation + assert_equal "utf8mb4_general_ci", CollationTest.columns_hash["string_ci_column"].collation end def test_case_sensitive - assert !CollationTest.columns_hash['string_ci_column'].case_sensitive? - assert CollationTest.columns_hash['string_cs_column'].case_sensitive? + assert_not_predicate CollationTest.columns_hash["string_ci_column"], :case_sensitive? + assert_predicate CollationTest.columns_hash["string_cs_column"], :case_sensitive? end def test_case_insensitive_comparison_for_ci_column - CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => false) - CollationTest.create!(:string_ci_column => 'A') - invalid = CollationTest.new(:string_ci_column => 'a') + CollationTest.validates_uniqueness_of(:string_ci_column, case_sensitive: false) + CollationTest.create!(string_ci_column: "A") + invalid = CollationTest.new(string_ci_column: "a") queries = assert_sql { invalid.save } ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) } assert_no_match(/lower/i, ci_uniqueness_query) end def test_case_insensitive_comparison_for_cs_column - CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => false) - CollationTest.create!(:string_cs_column => 'A') - invalid = CollationTest.new(:string_cs_column => 'a') + CollationTest.validates_uniqueness_of(:string_cs_column, case_sensitive: false) + CollationTest.create!(string_cs_column: "A") + invalid = CollationTest.new(string_cs_column: "a") queries = assert_sql { invalid.save } - cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/)} + cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } assert_match(/lower/i, cs_uniqueness_query) end def test_case_sensitive_comparison_for_ci_column - CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => true) - CollationTest.create!(:string_ci_column => 'A') - invalid = CollationTest.new(:string_ci_column => 'A') + CollationTest.validates_uniqueness_of(:string_ci_column, case_sensitive: true) + CollationTest.create!(string_ci_column: "A") + invalid = CollationTest.new(string_ci_column: "A") queries = assert_sql { invalid.save } ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) } assert_match(/binary/i, ci_uniqueness_query) end def test_case_sensitive_comparison_for_cs_column - CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => true) - CollationTest.create!(:string_cs_column => 'A') - invalid = CollationTest.new(:string_cs_column => 'A') + CollationTest.validates_uniqueness_of(:string_cs_column, case_sensitive: true) + CollationTest.create!(string_cs_column: "A") + invalid = CollationTest.new(string_cs_column: "A") queries = assert_sql { invalid.save } cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } assert_no_match(/binary/i, cs_uniqueness_query) @@ -54,8 +56,8 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase def test_case_sensitive_comparison_for_binary_column CollationTest.validates_uniqueness_of(:binary_column, case_sensitive: true) - CollationTest.create!(binary_column: 'A') - invalid = CollationTest.new(binary_column: 'A') + CollationTest.create!(binary_column: "A") + invalid = CollationTest.new(binary_column: "A") queries = assert_sql { invalid.save } bin_uniqueness_query = queries.detect { |q| q.match(/binary_column/) } assert_no_match(/\bBINARY\b/, bin_uniqueness_query) diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb index 668c07dacb..0bdbefdfb9 100644 --- a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb +++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require "cases/helper" -require 'support/schema_dumping_helper' +require "support/schema_dumping_helper" class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase include SchemaDumpingHelper @@ -8,8 +10,8 @@ class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase 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' + t.string :string_ascii_bin, charset: "ascii", collation: "ascii_bin" + t.text :text_ucs2_unicode_ci, charset: "ucs2", collation: "ucs2_unicode_ci" end end @@ -18,37 +20,37 @@ class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase end test "string column with charset and collation" do - column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' } + column = @connection.columns(:charset_collations).find { |c| c.name == "string_ascii_bin" } assert_equal :string, column.type - assert_equal 'ascii_bin', column.collation + 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' } + 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 + 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' + @connection.add_column :charset_collations, :title, :string, charset: "utf8mb4", collation: "utf8mb4_bin" - column = @connection.columns(:charset_collations).find { |c| c.name == 'title' } + column = @connection.columns(:charset_collations).find { |c| c.name == "title" } assert_equal :string, column.type - assert_equal 'utf8_bin', column.collation + assert_equal "utf8mb4_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' + @connection.add_column :charset_collations, :description, :string, charset: "utf8mb4", collation: "utf8mb4_unicode_ci" + @connection.change_column :charset_collations, :description, :text, charset: "utf8mb4", collation: "utf8mb4_general_ci" - column = @connection.columns(:charset_collations).find { |c| c.name == 'description' } + column = @connection.columns(:charset_collations).find { |c| c.name == "description" } assert_equal :text, column.type - assert_equal 'utf8_general_ci', column.collation + assert_equal "utf8mb4_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+collation: "ascii_bin"$}, output - assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output + assert_match %r{t\.string\s+"string_ascii_bin",\s+collation: "ascii_bin"$}, output + assert_match %r{t\.text\s+"text_ucs2_unicode_ci",\s+collation: "ucs2_unicode_ci"$}, output end end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index c4715393b3..9c6566106a 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require "cases/helper" -require 'support/connection_helper' +require "support/connection_helper" class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase include ConnectionHelper @@ -9,7 +11,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase def setup super @subscriber = SQLSubscriber.new - @subscription = ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber) + @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber) @connection = ActiveRecord::Base.connection end @@ -20,52 +22,79 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase def test_bad_connection assert_raise ActiveRecord::NoDatabaseError do - configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest') + configuration = ActiveRecord::Base.configurations["arunit"].merge(database: "inexistent_activerecord_unittest") connection = ActiveRecord::Base.mysql2_connection(configuration) - connection.drop_table 'ex', if_exists: true + connection.drop_table "ex", if_exists: true end end - def test_truncate - rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments") - count = rows.first.values.first - assert_operator count, :>, 0 - - ActiveRecord::Base.connection.truncate("comments") - rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments") - count = rows.first.values.first - assert_equal 0, count - end - def test_no_automatic_reconnection_after_timeout - assert @connection.active? - @connection.update('set @@wait_timeout=1') + assert_predicate @connection, :active? + @connection.update("set @@wait_timeout=1") sleep 2 - assert !@connection.active? - + assert_not_predicate @connection, :active? + ensure # Repair all fixture connections so other tests won't break. @fixture_connections.each(&:verify!) end def test_successful_reconnection_after_timeout_with_manual_reconnect - assert @connection.active? - @connection.update('set @@wait_timeout=1') + assert_predicate @connection, :active? + @connection.update("set @@wait_timeout=1") sleep 2 @connection.reconnect! - assert @connection.active? + assert_predicate @connection, :active? end def test_successful_reconnection_after_timeout_with_verify - assert @connection.active? - @connection.update('set @@wait_timeout=1') + assert_predicate @connection, :active? + @connection.update("set @@wait_timeout=1") sleep 2 @connection.verify! - assert @connection.active? + assert_predicate @connection, :active? + end + + def test_execute_after_disconnect + @connection.disconnect! + + error = assert_raise(ActiveRecord::StatementInvalid) do + @connection.execute("SELECT 1") + end + assert_kind_of Mysql2::Error, error.cause + end + + def test_quote_after_disconnect + @connection.disconnect! + + assert_raise(Mysql2::Error) do + @connection.quote("string") + end + end + + def test_active_after_disconnect + @connection.disconnect! + assert_equal false, @connection.active? + end + + def test_wait_timeout_as_string + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge(wait_timeout: "60")) + result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.wait_timeout") + assert_equal 60, result + end + end + + def test_wait_timeout_as_url + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge("url" => "mysql2:///?wait_timeout=60")) + result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.wait_timeout") + assert_equal 60, result + end end def test_mysql_connection_collation_is_configured - assert_equal 'utf8_unicode_ci', @connection.show_variable('collation_connection') - assert_equal 'utf8_general_ci', ARUnit2Model.connection.show_variable('collation_connection') + assert_equal "utf8mb4_unicode_ci", @connection.show_variable("collation_connection") + assert_equal "utf8mb4_general_ci", ARUnit2Model.connection.show_variable("collation_connection") end def test_mysql_default_in_strict_mode @@ -92,29 +121,29 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase def test_mysql_sql_mode_variable_overrides_strict_mode run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' })) - result = ActiveRecord::Base.connection.select_value('SELECT @@SESSION.sql_mode') + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { "sql_mode" => "ansi" })) + result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.sql_mode") assert_no_match %r(STRICT_ALL_TABLES), result end end - def test_passing_arbitary_flags_to_adapter + def test_passing_arbitrary_flags_to_adapter run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({flags: Mysql2::Client::COMPRESS})) - assert_equal (Mysql2::Client::COMPRESS | Mysql2::Client::FOUND_ROWS), ActiveRecord::Base.connection.raw_connection.query_options[:flags] + ActiveRecord::Base.establish_connection(orig_connection.merge(flags: Mysql2::Client::COMPRESS)) + assert_equal (Mysql2::Client::COMPRESS | Mysql2::Client::FOUND_ROWS), ActiveRecord::Base.connection.raw_connection.query_options[:flags] end end def test_passing_flags_by_array_to_adapter run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({flags: ['COMPRESS'] })) + ActiveRecord::Base.establish_connection(orig_connection.merge(flags: ["COMPRESS"])) assert_equal ["COMPRESS", "FOUND_ROWS"], ActiveRecord::Base.connection.raw_connection.query_options[:flags] end end def test_mysql_set_session_variable run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}})) + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { default_week_format: 3 })) session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT" assert_equal 3, session_mode.rows.first.first.to_i end @@ -122,7 +151,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase def test_mysql_set_session_variable_to_default run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}})) + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { default_week_format: :default })) global_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.DEFAULT_WEEK_FORMAT" session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT" assert_equal global_mode.rows, session_mode.rows @@ -130,21 +159,23 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end def test_logs_name_show_variable - @connection.show_variable 'foo' + ActiveRecord::Base.connection.materialize_transactions + @subscriber.logged.clear + @connection.show_variable "foo" assert_equal "SCHEMA", @subscriber.logged[0][1] end - def test_logs_name_rename_column_sql + def test_logs_name_rename_column_for_alter @connection.execute "CREATE TABLE `bar_baz` (`foo` varchar(255))" @subscriber.logged.clear - @connection.send(:rename_column_sql, 'bar_baz', 'foo', 'foo2') + @connection.send(:rename_column_for_alter, "bar_baz", "foo", "foo2") assert_equal "SCHEMA", @subscriber.logged[0][1] ensure @connection.execute "DROP TABLE `bar_baz`" end def test_get_and_release_advisory_lock - lock_name = "test_lock_name" + lock_name = "test lock'n'name" got_lock = @connection.get_advisory_lock(lock_name) assert got_lock, "get_advisory_lock should have returned true but it didn't" @@ -155,19 +186,19 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase released_lock = @connection.release_advisory_lock(lock_name) assert released_lock, "expected release_advisory_lock to return true but it didn't" - assert test_lock_free(lock_name), 'expected the test lock to be available after releasing' + assert test_lock_free(lock_name), "expected the test lock to be available after releasing" end def test_release_non_existent_advisory_lock - lock_name = "fake_lock_name" + lock_name = "fake lock'n'name" released_non_existent_lock = @connection.release_advisory_lock(lock_name) assert_equal released_non_existent_lock, false, - 'expected release_advisory_lock to return false when there was no lock to release' + "expected release_advisory_lock to return false when there was no lock to release" end - protected + private - def test_lock_free(lock_name) - @connection.select_value("SELECT IS_FREE_LOCK('#{lock_name}');") == 1 - end + def test_lock_free(lock_name) + @connection.select_value("SELECT IS_FREE_LOCK(#{@connection.quote(lock_name)})") == 1 + end end diff --git a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb new file mode 100644 index 0000000000..00a075e063 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true + +require "cases/helper" + +class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + test "microsecond precision for MySQL gte 5.6.4" do + stub_version "5.6.4" do + assert_microsecond_precision + end + end + + test "no microsecond precision for MySQL lt 5.6.4" do + stub_version "5.6.3" do + assert_no_microsecond_precision + end + end + + test "microsecond precision for MariaDB gte 5.3.0" do + stub_version "5.5.5-10.1.8-MariaDB-log" do + assert_microsecond_precision + end + end + + test "no microsecond precision for MariaDB lt 5.3.0" do + stub_version "5.2.9-MariaDB" do + assert_no_microsecond_precision + end + end + + private + def assert_microsecond_precision + assert_match_quoted_microsecond_datetime(/\.000001\z/) + end + + def assert_no_microsecond_precision + assert_match_quoted_microsecond_datetime(/\d\z/) + end + + def assert_match_quoted_microsecond_datetime(match) + assert_match match, @connection.quoted_date(Time.now.change(usec: 1)) + end + + def stub_version(full_version_string) + @connection.stub(:full_version, full_version_string) do + @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) + yield + end + ensure + @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) + end +end diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb index 35dbc76d1b..832f5d61d1 100644 --- a/activerecord/test/cases/adapters/mysql2/enum_test.rb +++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" class Mysql2EnumTest < ActiveRecord::Mysql2TestCase @@ -5,22 +7,17 @@ class Mysql2EnumTest < ActiveRecord::Mysql2TestCase end def test_enum_limit - column = EnumTest.columns_hash['enum_column'] + column = EnumTest.columns_hash["enum_column"] assert_equal 8, column.limit end - def test_should_not_be_blob_or_text_column - column = EnumTest.columns_hash['enum_column'] - assert_not column.blob_or_text_column? - end - def test_should_not_be_unsigned - column = EnumTest.columns_hash['enum_column'] - assert_not column.unsigned? + column = EnumTest.columns_hash["enum_column"] + assert_not_predicate column, :unsigned? end def test_should_not_be_bigint - column = EnumTest.columns_hash['enum_column'] - assert_not column.bigint? + column = EnumTest.columns_hash["enum_column"] + assert_not_predicate column, :bigint? end end diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb index b783b5fcd9..b8e778f0b0 100644 --- a/activerecord/test/cases/adapters/mysql2/explain_test.rb +++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb @@ -1,21 +1,23 @@ +# frozen_string_literal: true + require "cases/helper" -require 'models/developer' -require 'models/computer' +require "models/author" +require "models/post" class Mysql2ExplainTest < ActiveRecord::Mysql2TestCase - fixtures :developers + fixtures :authors def test_explain_for_one_query - explain = Developer.where(id: 1).explain - assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain - assert_match %r(developers |.* const), explain + explain = Author.where(id: 1).explain + assert_match %(EXPLAIN for: SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1), explain + assert_match %r(authors |.* const), explain end def test_explain_with_eager_loading - explain = Developer.where(id: 1).includes(:audit_logs).explain - assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain - assert_match %r(developers |.* const), explain - assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain - assert_match %r(audit_logs |.* ALL), explain + explain = Author.where(id: 1).includes(:posts).explain + assert_match %(EXPLAIN for: SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1), explain + assert_match %r(authors |.* const), explain + assert_match %(EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`author_id` = 1), explain + assert_match %r(posts |.* ALL), explain end end diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb index c8c933af5e..de78ba91f5 100644 --- a/activerecord/test/cases/adapters/mysql2/json_test.rb +++ b/activerecord/test/cases/adapters/mysql2/json_test.rb @@ -1,172 +1,24 @@ -require 'cases/helper' -require 'support/schema_dumping_helper' +# frozen_string_literal: true -if ActiveRecord::Base.connection.supports_json? -class Mysql2JSONTest < ActiveRecord::Mysql2TestCase - include SchemaDumpingHelper - self.use_transactional_tests = false - - class JsonDataType < ActiveRecord::Base - self.table_name = 'json_data_type' - - store_accessor :settings, :resolution - end +require "cases/helper" +require "cases/json_shared_test_cases" - def setup - @connection = ActiveRecord::Base.connection - begin - @connection.create_table('json_data_type') do |t| - t.json 'payload' - t.json 'settings' +if ActiveRecord::Base.connection.supports_json? + class Mysql2JSONTest < ActiveRecord::Mysql2TestCase + include JSONSharedTestCases + self.use_transactional_tests = false + + def setup + super + @connection.create_table("json_data_type") do |t| + t.json "payload" + t.json "settings" end end - end - - def teardown - @connection.drop_table :json_data_type, if_exists: true - JsonDataType.reset_column_information - end - - def test_column - column = JsonDataType.columns_hash["payload"] - assert_equal :json, column.type - assert_equal 'json', column.sql_type - - type = JsonDataType.type_for_attribute("payload") - assert_not type.binary? - end - - def test_change_table_supports_json - @connection.change_table('json_data_type') do |t| - t.json 'users' - end - JsonDataType.reset_column_information - column = JsonDataType.columns_hash['users'] - assert_equal :json, column.type - end - - def test_schema_dumping - output = dump_table_schema("json_data_type") - assert_match(/t\.json\s+"settings"/, output) - end - - def test_cast_value_on_write - x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar} - assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast) - assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload) - x.save - assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload) - end - - def test_type_cast_json - type = JsonDataType.type_for_attribute("payload") - - data = "{\"a_key\":\"a_value\"}" - hash = type.deserialize(data) - assert_equal({'a_key' => 'a_value'}, hash) - assert_equal({'a_key' => 'a_value'}, type.deserialize(data)) - - assert_equal({}, type.deserialize("{}")) - assert_equal({'key'=>nil}, type.deserialize('{"key": null}')) - assert_equal({'c'=>'}','"a"'=>'b "a b'}, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) - end - - def test_rewrite - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - x.payload = { '"a\'' => 'b' } - assert x.save! - end - - def test_select - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - assert_equal({'k' => 'v'}, x.payload) - end - - def test_select_multikey - @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')| - x = JsonDataType.first - assert_equal({'k1' => 'v1', 'k2' => 'v2', 'k3' => [1,2,3]}, x.payload) - end - def test_null_json - @connection.execute %q|insert into json_data_type (payload) VALUES(null)| - x = JsonDataType.first - assert_equal(nil, x.payload) - end - - def test_select_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.first - assert_equal(['v0', {'k1' => 'v1'}], x.payload) - end - - def test_rewrite_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.first - x.payload = ['v1', {'k2' => 'v2'}, 'v3'] - assert x.save! - end - - def test_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - x.save! - x = JsonDataType.first - assert_equal "320×480", x.resolution - - x.resolution = "640×1136" - x.save! - - x = JsonDataType.first - assert_equal "640×1136", x.resolution - end - - def test_duplication_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = x.dup - assert_equal "320×480", y.resolution - end - - def test_yaml_round_trip_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = YAML.load(YAML.dump(x)) - assert_equal "320×480", y.resolution - end - - def test_changes_in_place - json = JsonDataType.new - assert_not json.changed? - - json.payload = { 'one' => 'two' } - assert json.changed? - assert json.payload_changed? - - json.save! - assert_not json.changed? - - json.payload['three'] = 'four' - assert json.payload_changed? - - json.save! - json.reload - - assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload) - assert_not json.changed? - end - - def test_assigning_invalid_json - json = JsonDataType.new - - json.payload = 'foo' - - assert_nil json.payload + private + def column_type + :json + end end end -end diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb index 00d23740b6..6ade2eec24 100644 --- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/ddl_helper" @@ -11,8 +13,8 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase def test_exec_query_nothing_raises_with_no_result_queries assert_nothing_raised do with_example_table do - @conn.exec_query('INSERT INTO ex (number) VALUES (1)') - @conn.exec_query('DELETE FROM ex WHERE number = 1') + @conn.exec_query("INSERT INTO ex (number) VALUES (1)") + @conn.exec_query("DELETE FROM ex WHERE number = 1") end end end @@ -23,25 +25,25 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase end def test_columns_for_distinct_one_order - assert_equal "posts.id, posts.created_at AS alias_0", + assert_equal "posts.created_at AS alias_0, posts.id", @conn.columns_for_distinct("posts.id", ["posts.created_at desc"]) end def test_columns_for_distinct_few_orders - assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1", + assert_equal "posts.created_at AS alias_0, posts.position AS alias_1, posts.id", @conn.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"]) end def test_columns_for_distinct_with_case assert_equal( - 'posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0', - @conn.columns_for_distinct('posts.id', + "CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0, posts.id", + @conn.columns_for_distinct("posts.id", ["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"]) ) end def test_columns_for_distinct_blank_not_nil_orders - assert_equal "posts.id, posts.created_at AS alias_0", + assert_equal "posts.created_at AS alias_0, posts.id", @conn.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "]) end @@ -50,13 +52,169 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase def order.to_sql "posts.created_at desc" end - assert_equal "posts.id, posts.created_at AS alias_0", + assert_equal "posts.created_at AS alias_0, posts.id", @conn.columns_for_distinct("posts.id", [order]) end - private + def test_errors_for_bigint_fks_on_integer_pk_table_in_alter_table + # table old_cars has primary key of integer + + error = assert_raises(ActiveRecord::MismatchedForeignKey) do + @conn.add_reference :engines, :old_car + @conn.add_foreign_key :engines, :old_cars + end + + assert_includes error.message, <<~MSG.squish + Column `old_car_id` on table `engines` does not match column `id` on `old_cars`, + which has type `int(11)`. To resolve this issue, change the type of the `old_car_id` + column on `engines` to be :integer. (For example `t.integer :old_car_id`). + MSG + assert_not_nil error.cause + ensure + @conn.execute("ALTER TABLE engines DROP COLUMN old_car_id") rescue nil + end + + def test_errors_for_bigint_fks_on_integer_pk_table_in_create_table + # table old_cars has primary key of integer + + error = assert_raises(ActiveRecord::MismatchedForeignKey) do + @conn.execute(<<~SQL) + CREATE TABLE activerecord_unittest.foos ( + id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, + old_car_id bigint, + INDEX index_foos_on_old_car_id (old_car_id), + CONSTRAINT fk_rails_ff771f3c96 FOREIGN KEY (old_car_id) REFERENCES old_cars (id) + ) + SQL + end + + assert_includes error.message, <<~MSG.squish + Column `old_car_id` on table `foos` does not match column `id` on `old_cars`, + which has type `int(11)`. To resolve this issue, change the type of the `old_car_id` + column on `foos` to be :integer. (For example `t.integer :old_car_id`). + MSG + assert_not_nil error.cause + ensure + @conn.drop_table :foos, if_exists: true + end + + def test_errors_for_integer_fks_on_bigint_pk_table_in_create_table + # table old_cars has primary key of bigint + + error = assert_raises(ActiveRecord::MismatchedForeignKey) do + @conn.execute(<<~SQL) + CREATE TABLE activerecord_unittest.foos ( + id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, + car_id int, + INDEX index_foos_on_car_id (car_id), + CONSTRAINT fk_rails_ff771f3c96 FOREIGN KEY (car_id) REFERENCES cars (id) + ) + SQL + end + + assert_includes error.message, <<~MSG.squish + Column `car_id` on table `foos` does not match column `id` on `cars`, + which has type `bigint(20)`. To resolve this issue, change the type of the `car_id` + column on `foos` to be :bigint. (For example `t.bigint :car_id`). + MSG + assert_not_nil error.cause + ensure + @conn.drop_table :foos, if_exists: true + end + + def test_errors_for_bigint_fks_on_string_pk_table_in_create_table + # table old_cars has primary key of string + + error = assert_raises(ActiveRecord::MismatchedForeignKey) do + @conn.execute(<<~SQL) + CREATE TABLE activerecord_unittest.foos ( + id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY, + subscriber_id bigint, + INDEX index_foos_on_subscriber_id (subscriber_id), + CONSTRAINT fk_rails_ff771f3c96 FOREIGN KEY (subscriber_id) REFERENCES subscribers (nick) + ) + SQL + end + + assert_includes error.message, <<~MSG.squish + Column `subscriber_id` on table `foos` does not match column `nick` on `subscribers`, + which has type `varchar(255)`. To resolve this issue, change the type of the `subscriber_id` + column on `foos` to be :string. (For example `t.string :subscriber_id`). + MSG + assert_not_nil error.cause + ensure + @conn.drop_table :foos, if_exists: true + end + + def test_errors_when_an_insert_query_is_called_while_preventing_writes + assert_raises(ActiveRecord::ReadOnlyError) do + @conn.while_preventing_writes do + @conn.insert("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + end + end + end + + def test_errors_when_an_update_query_is_called_while_preventing_writes + @conn.insert("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @conn.while_preventing_writes do + @conn.update("UPDATE `engines` SET `engines`.`car_id` = '9989' WHERE `engines`.`car_id` = '138853948594'") + end + end + end + + def test_errors_when_a_delete_query_is_called_while_preventing_writes + @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @conn.while_preventing_writes do + @conn.execute("DELETE FROM `engines` where `engines`.`car_id` = '138853948594'") + end + end + end + + def test_errors_when_a_replace_query_is_called_while_preventing_writes + @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @conn.while_preventing_writes do + @conn.execute("REPLACE INTO `engines` SET `engines`.`car_id` = '249823948'") + end + end + end + + def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes + @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + + @conn.while_preventing_writes do + assert_equal 1, @conn.execute("SELECT `engines`.* FROM `engines` WHERE `engines`.`car_id` = '138853948594'").entries.count + end + end + + def test_doesnt_error_when_a_show_query_is_called_while_preventing_writes + @conn.while_preventing_writes do + assert_equal 2, @conn.execute("SHOW FULL FIELDS FROM `engines`").entries.count + end + end + + def test_doesnt_error_when_a_set_query_is_called_while_preventing_writes + @conn.while_preventing_writes do + assert_nil @conn.execute("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci") + end + end + + def test_doesnt_error_when_a_read_query_with_leading_chars_is_called_while_preventing_writes + @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") - def with_example_table(definition = 'id int auto_increment primary key, number int, data varchar(255)', &block) - super(@conn, 'ex', definition, &block) + @conn.while_preventing_writes do + assert_equal 1, @conn.execute("(\n( SELECT `engines`.* FROM `engines` WHERE `engines`.`car_id` = '138853948594' ) )").entries.count + end end + + private + + def with_example_table(definition = "id int auto_increment primary key, number int, data varchar(255)", &block) + super(@conn, "ex", definition, &block) + end end diff --git a/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb b/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb new file mode 100644 index 0000000000..b9794c5710 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/post" + +if supports_optimizer_hints? + class Mysql2OptimzerHintsTest < ActiveRecord::Mysql2TestCase + fixtures :posts + + def test_optimizer_hints + assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do + posts = Post.optimizer_hints("NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id)") + posts = posts.select(:id).where(author_id: [0, 1]) + assert_includes posts.explain, "| index | index_posts_on_author_id | index_posts_on_author_id |" + end + end + + def test_optimizer_hints_is_sanitized + assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do + posts = Post.optimizer_hints("/*+ NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id) */") + posts = posts.select(:id).where(author_id: [0, 1]) + assert_includes posts.explain, "| index | index_posts_on_author_id | index_posts_on_author_id |" + end + + assert_sql(%r{\ASELECT /\*\+ `posts`\.\*, \*/}) do + posts = Post.optimizer_hints("**// `posts`.*, //**") + posts = posts.select(:id).where(author_id: [0, 1]) + assert_equal({ "id" => 1 }, posts.first.as_json) + end + end + + def test_optimizer_hints_with_unscope + assert_sql(%r{\ASELECT `posts`\.`id`}) do + posts = Post.optimizer_hints("/*+ NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id) */") + posts = posts.select(:id).where(author_id: [0, 1]) + posts.unscope(:optimizer_hints).load + end + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/quoting_test.rb b/activerecord/test/cases/adapters/mysql2/quoting_test.rb deleted file mode 100644 index 2de7e1b526..0000000000 --- a/activerecord/test/cases/adapters/mysql2/quoting_test.rb +++ /dev/null @@ -1,21 +0,0 @@ -require "cases/helper" - -class Mysql2QuotingTest < ActiveRecord::Mysql2TestCase - setup do - @connection = ActiveRecord::Base.connection - end - - test 'quoted date precision for gte 5.6.4' do - @connection.stubs(:full_version).returns('5.6.4') - @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) - t = Time.now.change(usec: 1) - assert_match(/\.000001\z/, @connection.quoted_date(t)) - end - - test 'quoted date precision for lt 5.6.4' do - @connection.stubs(:full_version).returns('5.6.3') - @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) - t = Time.now.change(usec: 1) - assert_no_match(/\.000001\z/, @connection.quoted_date(t)) - end -end diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb deleted file mode 100644 index ffb4e2c5cf..0000000000 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ /dev/null @@ -1,152 +0,0 @@ -require "cases/helper" - -# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with -# reserved word names (ie: group, order, values, etc...) -class Mysql2ReservedWordTest < ActiveRecord::Mysql2TestCase - class Group < ActiveRecord::Base - Group.table_name = 'group' - belongs_to :select - has_one :values - end - - class Select < ActiveRecord::Base - Select.table_name = 'select' - has_many :groups - end - - class Values < ActiveRecord::Base - Values.table_name = 'values' - end - - class Distinct < ActiveRecord::Base - Distinct.table_name = 'distinct' - has_and_belongs_to_many :selects - has_many :values, :through => :groups - end - - def setup - @connection = ActiveRecord::Base.connection - - # we call execute directly here (and do similar below) because ActiveRecord::Base#create_table() - # will fail with these table names if these test cases fail - - create_tables_directly 'group'=>'id int auto_increment primary key, `order` varchar(255), select_id int', - 'select'=>'id int auto_increment primary key', - 'values'=>'id int auto_increment primary key, group_id int', - 'distinct'=>'id int auto_increment primary key', - 'distinct_select'=>'distinct_id int, select_id int' - end - - teardown do - drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order'] - end - - # create tables with reserved-word names and columns - def test_create_tables - assert_nothing_raised { - @connection.create_table :order do |t| - t.column :group, :string - end - } - end - - # rename tables with reserved-word names - def test_rename_tables - assert_nothing_raised { @connection.rename_table(:group, :order) } - end - - # alter column with a reserved-word name in a table with a reserved-word name - def test_change_columns - assert_nothing_raised { @connection.change_column_default(:group, :order, 'whatever') } - #the quoting here will reveal any double quoting issues in change_column's interaction with the column method in the adapter - assert_nothing_raised { @connection.change_column('group', 'order', :Int, :default => 0) } - assert_nothing_raised { @connection.rename_column(:group, :order, :values) } - end - - # introspect table with reserved word name - def test_introspect - assert_nothing_raised { @connection.columns(:group) } - assert_nothing_raised { @connection.indexes(:group) } - end - - #fixtures - self.use_instantiated_fixtures = true - self.use_transactional_tests = false - - #activerecord model class with reserved-word table name - def test_activerecord_model - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - x = nil - assert_nothing_raised { x = Group.new } - x.order = 'x' - assert_nothing_raised { x.save } - x.order = 'y' - assert_nothing_raised { x.save } - assert_nothing_raised { Group.find_by_order('y') } - assert_nothing_raised { Group.find(1) } - end - - # has_one association with reserved-word table name - def test_has_one_associations - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - v = nil - assert_nothing_raised { v = Group.find(1).values } - assert_equal 2, v.id - end - - # belongs_to association with reserved-word table name - def test_belongs_to_associations - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - gs = nil - assert_nothing_raised { gs = Select.find(2).groups } - assert_equal gs.length, 2 - assert(gs.collect(&:id).sort == [2, 3]) - end - - # has_and_belongs_to_many with reserved-word table name - def test_has_and_belongs_to_many - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - s = nil - assert_nothing_raised { s = Distinct.find(1).selects } - assert_equal s.length, 2 - assert(s.collect(&:id).sort == [1, 2]) - end - - # activerecord model introspection with reserved-word table and column names - def test_activerecord_introspection - assert_nothing_raised { Group.table_exists? } - assert_nothing_raised { Group.columns } - end - - # Calculations - def test_calculations_work_with_reserved_words - assert_nothing_raised { Group.count } - end - - def test_associations_work_with_reserved_words - assert_nothing_raised { Select.all.merge!(:includes => [:groups]).to_a } - end - - #the following functions were added to DRY test cases - - private - # custom fixture loader, uses FixtureSet#create_fixtures and appends base_path to the current file's path - def create_test_fixtures(*fixture_names) - ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) - end - - # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name - def drop_tables_directly(table_names, connection = @connection) - table_names.each do |name| - connection.drop_table name, if_exists: true - end - end - - # custom create table, uses execute on connection to create a table, note: escapes table_name, does NOT escape columns - def create_tables_directly (tables, connection = @connection) - tables.each do |table_name, column_properties| - connection.execute("CREATE TABLE `#{table_name}` ( #{column_properties} )") - end - end - -end diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index 7c89fda582..d7d9a2d732 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -1,6 +1,10 @@ +# frozen_string_literal: true + require "cases/helper" class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase + self.use_transactional_tests = false + def test_renaming_index_on_foreign_key connection.add_index "engines", "car_id" connection.add_foreign_key :engines, :cars, name: "fk_engines_cars" @@ -16,9 +20,9 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase table_name = ActiveRecord::SchemaMigration.table_name connection.drop_table table_name, if_exists: true - connection.initialize_schema_migrations_table + ActiveRecord::SchemaMigration.create_table - assert connection.column_exists?(table_name, :version, :string, collation: 'utf8_general_ci') + assert connection.column_exists?(table_name, :version, :string) end end @@ -27,33 +31,35 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase table_name = ActiveRecord::InternalMetadata.table_name connection.drop_table table_name, if_exists: true - connection.initialize_internal_metadata_table + ActiveRecord::InternalMetadata.create_table - assert connection.column_exists?(table_name, :key, :string, collation: 'utf8_general_ci') + assert connection.column_exists?(table_name, :key, :string) end + ensure + ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment end private - def with_encoding_utf8mb4 - database_name = connection.current_database - database_info = connection.select_one("SELECT * FROM information_schema.schemata WHERE schema_name = '#{database_name}'") + def with_encoding_utf8mb4 + database_name = connection.current_database + database_info = connection.select_one("SELECT * FROM information_schema.schemata WHERE schema_name = '#{database_name}'") - original_charset = database_info["DEFAULT_CHARACTER_SET_NAME"] - original_collation = database_info["DEFAULT_COLLATION_NAME"] + original_charset = database_info["DEFAULT_CHARACTER_SET_NAME"] + original_collation = database_info["DEFAULT_COLLATION_NAME"] - execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET utf8mb4") + execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET utf8mb4") - yield - ensure - execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}") - end + yield + ensure + execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}") + end - def connection - @connection ||= ActiveRecord::Base.connection - end + def connection + @connection ||= ActiveRecord::Base.connection + end - def execute(sql) - connection.execute(sql) - end + def execute(sql) + connection.execute(sql) + end end diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 43957791b1..1283b0642c 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + require "cases/helper" -require 'models/post' -require 'models/comment' +require "models/post" +require "models/comment" module ActiveRecord module ConnectionAdapters @@ -16,7 +18,7 @@ module ActiveRecord @omgpost = Class.new(ActiveRecord::Base) do self.inheritance_column = :disabled self.table_name = "#{db}.#{table}" - def self.name; 'Post'; end + def self.name; "Post"; end end end @@ -31,13 +33,13 @@ module ActiveRecord t.float :float_25, limit: 25 end - column_no_limit = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_no_limit' } - column_short = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_short' } - column_long = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_long' } + column_no_limit = @connection.columns(:mysql_doubles).find { |c| c.name == "float_no_limit" } + column_short = @connection.columns(:mysql_doubles).find { |c| c.name == "float_short" } + column_long = @connection.columns(:mysql_doubles).find { |c| c.name == "float_long" } - column_23 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_23' } - column_24 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_24' } - column_25 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_25' } + column_23 = @connection.columns(:mysql_doubles).find { |c| c.name == "float_23" } + column_24 = @connection.columns(:mysql_doubles).find { |c| c.name == "float_24" } + column_25 = @connection.columns(:mysql_doubles).find { |c| c.name == "float_25" } # Mysql floats are precision 0..24, Mysql doubles are precision 25..53 assert_equal 24, column_no_limit.limit @@ -56,7 +58,7 @@ module ActiveRecord end def test_primary_key - assert_equal 'id', @omgpost.primary_key + assert_equal "id", @omgpost.primary_key end def test_data_source_exists? @@ -65,22 +67,22 @@ module ActiveRecord end def test_data_source_exists_wrong_schema - assert(!@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist") + assert_not(@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist") end def test_dump_indexes - index_a_name = 'index_key_tests_on_snack' - index_b_name = 'index_key_tests_on_pizza' - index_c_name = 'index_key_tests_on_awesome' + index_a_name = "index_key_tests_on_snack" + index_b_name = "index_key_tests_on_pizza" + index_c_name = "index_key_tests_on_awesome" - table = 'key_tests' + table = "key_tests" indexes = @connection.indexes(table).sort_by(&:name) - assert_equal 3,indexes.size + assert_equal 3, indexes.size - index_a = indexes.select{|i| i.name == index_a_name}[0] - index_b = indexes.select{|i| i.name == index_b_name}[0] - index_c = indexes.select{|i| i.name == index_c_name}[0] + index_a = indexes.select { |i| i.name == index_a_name }[0] + index_b = indexes.select { |i| i.name == index_b_name }[0] + index_c = indexes.select { |i| i.name == index_c_name }[0] assert_equal :btree, index_a.using assert_nil index_a.type assert_equal :btree, index_b.using @@ -103,3 +105,24 @@ module ActiveRecord end end end + +class Mysql2AnsiQuotesTest < ActiveRecord::Mysql2TestCase + def setup + @connection = ActiveRecord::Base.connection + @connection.execute("SET SESSION sql_mode='ANSI_QUOTES'") + end + + def teardown + @connection.reconnect! + end + + def test_primary_key_method_with_ansi_quotes + assert_equal "id", @connection.primary_key("topics") + end + + def test_foreign_keys_method_with_ansi_quotes + fks = @connection.foreign_keys("lessons_students") + assert_equal([["lessons_students", "students", :cascade]], + fks.map { |fk| [fk.from_table, fk.to_table, fk.on_delete] }) + end +end diff --git a/activerecord/test/cases/adapters/mysql2/sp_test.rb b/activerecord/test/cases/adapters/mysql2/sp_test.rb index 4197ba45f1..7b6dce71e9 100644 --- a/activerecord/test/cases/adapters/mysql2/sp_test.rb +++ b/activerecord/test/cases/adapters/mysql2/sp_test.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + require "cases/helper" -require 'models/topic' -require 'models/reply' +require "models/topic" +require "models/reply" class Mysql2StoredProcedureTest < ActiveRecord::Mysql2TestCase fixtures :topics def setup @connection = ActiveRecord::Base.connection - unless ActiveRecord::Base.connection.version >= '5.6.0' + unless ActiveRecord::Base.connection.version >= "5.6.0" skip("no stored procedure support") end end @@ -15,21 +17,21 @@ class Mysql2StoredProcedureTest < ActiveRecord::Mysql2TestCase # Test that MySQL allows multiple results for stored procedures # # In MySQL 5.6, CLIENT_MULTI_RESULTS is enabled by default. - # http://dev.mysql.com/doc/refman/5.6/en/call.html + # https://dev.mysql.com/doc/refman/5.6/en/call.html def test_multi_results - rows = @connection.select_rows('CALL ten();') + rows = @connection.select_rows("CALL ten();") assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}" assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select_rows'" end def test_multi_results_from_select_one - row = @connection.select_one('CALL topics(1);') - assert_equal 'David', row['author_name'] + row = @connection.select_one("CALL topics(1);") + assert_equal "David", row["author_name"] assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select_one'" end def test_multi_results_from_find_by_sql - topics = Topic.find_by_sql 'CALL topics(3);' + topics = Topic.find_by_sql "CALL topics(3);" assert_equal 3, topics.size assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select'" end diff --git a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb index 4926bc2267..e10642cbb4 100644 --- a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb +++ b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + require "cases/helper" class Mysql2SqlTypesTest < ActiveRecord::Mysql2TestCase def test_binary_types - assert_equal 'varbinary(64)', type_to_sql(:binary, 64) - assert_equal 'varbinary(4095)', type_to_sql(:binary, 4095) - assert_equal 'blob', type_to_sql(:binary, 4096) - assert_equal 'blob', type_to_sql(:binary) + assert_equal "varbinary(64)", type_to_sql(:binary, 64) + assert_equal "varbinary(4095)", type_to_sql(:binary, 4095) + assert_equal "blob", type_to_sql(:binary, 4096) + assert_equal "blob", type_to_sql(:binary) end - def type_to_sql(*args) - ActiveRecord::Base.connection.type_to_sql(*args) + def type_to_sql(type, limit = nil) + ActiveRecord::Base.connection.type_to_sql(type, limit: limit) end end diff --git a/activerecord/test/cases/adapters/mysql2/table_options_test.rb b/activerecord/test/cases/adapters/mysql2/table_options_test.rb index af121ee7d9..1c92df940f 100644 --- a/activerecord/test/cases/adapters/mysql2/table_options_test.rb +++ b/activerecord/test/cases/adapters/mysql2/table_options_test.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + require "cases/helper" -require 'support/schema_dumping_helper' +require "support/schema_dumping_helper" class Mysql2TableOptionsTest < ActiveRecord::Mysql2TestCase include SchemaDumpingHelper @@ -15,28 +17,103 @@ class Mysql2TableOptionsTest < ActiveRecord::Mysql2TestCase test "table options with ENGINE" do @connection.create_table "mysql_table_options", force: true, options: "ENGINE=MyISAM" output = dump_table_schema("mysql_table_options") - options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] assert_match %r{ENGINE=MyISAM}, options end test "table options with ROW_FORMAT" do @connection.create_table "mysql_table_options", force: true, options: "ROW_FORMAT=REDUNDANT" output = dump_table_schema("mysql_table_options") - options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] assert_match %r{ROW_FORMAT=REDUNDANT}, options end test "table options with CHARSET" do @connection.create_table "mysql_table_options", force: true, options: "CHARSET=utf8mb4" output = dump_table_schema("mysql_table_options") - options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] assert_match %r{CHARSET=utf8mb4}, options end test "table options with COLLATE" do @connection.create_table "mysql_table_options", force: true, options: "COLLATE=utf8mb4_bin" output = dump_table_schema("mysql_table_options") - options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] assert_match %r{COLLATE=utf8mb4_bin}, options end end + +class Mysql2DefaultEngineOptionSchemaDumpTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + self.use_transactional_tests = false + + def setup + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + end + + def teardown + ActiveRecord::Base.connection.drop_table "mysql_table_options", if_exists: true + ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::SchemaMigration.delete_all rescue nil + end + + test "schema dump includes ENGINE=InnoDB if not provided" do + ActiveRecord::Base.connection.create_table "mysql_table_options", force: true + + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{ENGINE=InnoDB}, options + end + + test "schema dump includes ENGINE=InnoDB in legacy migrations" do + migration = Class.new(ActiveRecord::Migration[5.1]) do + def migrate(x) + create_table "mysql_table_options", force: true + end + end.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{ENGINE=InnoDB}, options + end +end + +class Mysql2DefaultEngineOptionSqlOutputTest < ActiveRecord::Mysql2TestCase + self.use_transactional_tests = false + + def setup + @logger_was = ActiveRecord::Base.logger + @log = StringIO.new + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Base.logger = ActiveSupport::Logger.new(@log) + ActiveRecord::Migration.verbose = false + end + + def teardown + ActiveRecord::Base.logger = @logger_was + ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::Base.connection.drop_table "mysql_table_options", if_exists: true + ActiveRecord::SchemaMigration.delete_all rescue nil + end + + test "new migrations do not contain default ENGINE=InnoDB option" do + ActiveRecord::Base.connection.create_table "mysql_table_options", force: true + + assert_no_match %r{ENGINE=InnoDB}, @log.string + end + + test "legacy migrations contain default ENGINE=InnoDB option" do + migration = Class.new(ActiveRecord::Migration[5.1]) do + def migrate(x) + create_table "mysql_table_options", force: true + end + end.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert_match %r{ENGINE=InnoDB}, @log.string + end +end diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb new file mode 100644 index 0000000000..52e283f247 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb @@ -0,0 +1,149 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/connection_helper" + +module ActiveRecord + class Mysql2TransactionTest < ActiveRecord::Mysql2TestCase + self.use_transactional_tests = false + + class Sample < ActiveRecord::Base + self.table_name = "samples" + end + + setup do + @abort, Thread.abort_on_exception = Thread.abort_on_exception, false + Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception + + @connection = ActiveRecord::Base.connection + @connection.clear_cache! + + @connection.transaction do + @connection.drop_table "samples", if_exists: true + @connection.create_table("samples") do |t| + t.integer "value" + end + end + + Sample.reset_column_information + end + + teardown do + @connection.drop_table "samples", if_exists: true + + Thread.abort_on_exception = @abort + Thread.report_on_exception = @original_report_on_exception + end + + test "raises Deadlocked when a deadlock is encountered" do + assert_raises(ActiveRecord::Deadlocked) do + barrier = Concurrent::CyclicBarrier.new(2) + + s1 = Sample.create value: 1 + s2 = Sample.create value: 2 + + thread = Thread.new do + Sample.transaction do + s1.lock! + barrier.wait + s2.update value: 1 + end + end + + begin + Sample.transaction do + s2.lock! + barrier.wait + s1.update value: 2 + end + ensure + thread.join + end + end + end + + test "raises LockWaitTimeout when lock wait timeout exceeded" do + assert_raises(ActiveRecord::LockWaitTimeout) do + s = Sample.create!(value: 1) + latch1 = Concurrent::CountDownLatch.new + latch2 = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch1.count_down + latch2.wait + end + end + + begin + Sample.transaction do + latch1.wait + Sample.connection.execute("SET innodb_lock_wait_timeout = 1") + Sample.lock.find(s.id) + end + ensure + Sample.connection.execute("SET innodb_lock_wait_timeout = DEFAULT") + latch2.count_down + thread.join + end + end + end + + test "raises StatementTimeout when statement timeout exceeded" do + skip unless ActiveRecord::Base.connection.show_variable("max_execution_time") + assert_raises(ActiveRecord::StatementTimeout) do + s = Sample.create!(value: 1) + latch1 = Concurrent::CountDownLatch.new + latch2 = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch1.count_down + latch2.wait + end + end + + begin + Sample.transaction do + latch1.wait + Sample.connection.execute("SET max_execution_time = 1") + Sample.lock.find(s.id) + end + ensure + Sample.connection.execute("SET max_execution_time = DEFAULT") + latch2.count_down + thread.join + end + end + end + + test "raises QueryCanceled when canceling statement due to user request" do + assert_raises(ActiveRecord::QueryCanceled) do + s = Sample.create!(value: 1) + latch = Concurrent::CountDownLatch.new + + thread = Thread.new do + Sample.transaction do + Sample.lock.find(s.id) + latch.count_down + sleep(0.5) + conn = Sample.connection + pid = conn.query_value("SELECT id FROM information_schema.processlist WHERE info LIKE '% FOR UPDATE'") + conn.execute("KILL QUERY #{pid}") + end + end + + begin + Sample.transaction do + latch.wait + Sample.lock.find(s.id) + end + ensure + thread.join + end + end + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb index 0a9703263e..97da96003d 100644 --- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require "cases/helper" require "support/schema_dumping_helper" @@ -15,6 +17,7 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase t.bigint :unsigned_bigint, unsigned: true t.float :unsigned_float, unsigned: true t.decimal :unsigned_decimal, unsigned: true, precision: 10, scale: 2 + t.column :unsigned_zerofill, "int unsigned zerofill" end end @@ -28,16 +31,16 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase end test "minus value is out of range" do - assert_raise(RangeError) do + assert_raise(ActiveModel::RangeError) do UnsignedType.create(unsigned_integer: -10) end - assert_raise(RangeError) do + assert_raise(ActiveModel::RangeError) do UnsignedType.create(unsigned_bigint: -10) end - assert_raise(ActiveRecord::StatementInvalid) do + assert_raise(ActiveRecord::RangeError) do UnsignedType.create(unsigned_float: -10.0) end - assert_raise(ActiveRecord::StatementInvalid) do + assert_raise(ActiveRecord::RangeError) do UnsignedType.create(unsigned_decimal: -10.0) end end @@ -50,16 +53,16 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase t.unsigned_decimal :unsigned_decimal_t, precision: 10, scale: 2 end - @connection.columns("unsigned_types").select { |c| /^unsigned_/ === c.name }.each do |column| - assert column.unsigned? + @connection.columns("unsigned_types").select { |c| /^unsigned_/.match?(c.name) }.each do |column| + assert_predicate column, :unsigned? end end test "schema dump includes unsigned option" do schema = dump_table_schema "unsigned_types" - assert_match %r{t.integer\s+"unsigned_integer",\s+unsigned: true$}, schema - assert_match %r{t.bigint\s+"unsigned_bigint",\s+unsigned: true$}, schema - assert_match %r{t.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema - assert_match %r{t.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema + assert_match %r{t\.integer\s+"unsigned_integer",\s+unsigned: true$}, schema + assert_match %r{t\.bigint\s+"unsigned_bigint",\s+unsigned: true$}, schema + assert_match %r{t\.float\s+"unsigned_float",\s+unsigned: true$}, schema + assert_match %r{t\.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema end end diff --git a/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb new file mode 100644 index 0000000000..8494acee3b --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +if ActiveRecord::Base.connection.supports_virtual_columns? + class Mysql2VirtualColumnTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + + self.use_transactional_tests = false + + class VirtualColumn < ActiveRecord::Base + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :virtual_columns, force: true do |t| + t.string :name + t.virtual :upper_name, type: :string, as: "UPPER(`name`)" + t.virtual :name_length, type: :integer, as: "LENGTH(`name`)", stored: true + t.virtual :name_octet_length, type: :integer, as: "OCTET_LENGTH(`name`)", stored: true + end + VirtualColumn.create(name: "Rails") + end + + def teardown + @connection.drop_table :virtual_columns, if_exists: true + VirtualColumn.reset_column_information + end + + def test_virtual_column + column = VirtualColumn.columns_hash["upper_name"] + assert_predicate column, :virtual? + assert_match %r{\bVIRTUAL\b}, column.extra + assert_equal "RAILS", VirtualColumn.take.upper_name + end + + def test_stored_column + column = VirtualColumn.columns_hash["name_length"] + assert_predicate column, :virtual? + assert_match %r{\b(?:STORED|PERSISTENT)\b}, column.extra + assert_equal 5, VirtualColumn.take.name_length + end + + def test_change_table + @connection.change_table :virtual_columns do |t| + t.virtual :lower_name, type: :string, as: "LOWER(name)" + end + VirtualColumn.reset_column_information + column = VirtualColumn.columns_hash["lower_name"] + assert_predicate column, :virtual? + assert_match %r{\bVIRTUAL\b}, column.extra + assert_equal "rails", VirtualColumn.take.lower_name + end + + def test_schema_dumping + output = dump_table_schema("virtual_columns") + assert_match(/t\.virtual\s+"upper_name",\s+type: :string,\s+as: "(?:UPPER|UCASE)\(`name`\)"$/i, output) + assert_match(/t\.virtual\s+"name_length",\s+type: :integer,\s+as: "(?:octet_length|length)\(`name`\)",\s+stored: true$/i, output) + assert_match(/t\.virtual\s+"name_octet_length",\s+type: :integer,\s+as: "(?:octet_length|length)\(`name`\)",\s+stored: true$/i, output) + end + end +end |