diff options
Diffstat (limited to 'activerecord/test/cases')
217 files changed, 8217 insertions, 3764 deletions
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 1712ff0ac6..34e3bc9d66 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require "models/book" require "models/post" require "models/author" +require "models/event" module ActiveRecord class AdapterTest < ActiveRecord::TestCase @@ -11,7 +12,8 @@ module ActiveRecord ## # PostgreSQL does not support null bytes in strings - unless current_adapter?(:PostgreSQLAdapter) + unless current_adapter?(:PostgreSQLAdapter) || + (current_adapter?(:SQLite3Adapter) && !ActiveRecord::Base.connection.prepared_statements) def test_update_prepared_statement b = Book.create(name: "my \x00 book") b.reload @@ -22,8 +24,15 @@ module ActiveRecord end end + def test_create_record_with_pk_as_zero + Book.create(id: 0) + assert_equal 0, Book.find(0).id + assert_nothing_raised { Book.destroy(0) } + end + def test_tables - tables = @connection.tables + tables = nil + ActiveSupport::Deprecation.silence { tables = @connection.tables } assert tables.include?("accounts") assert tables.include?("authors") assert tables.include?("tasks") @@ -31,9 +40,30 @@ module ActiveRecord end def test_table_exists? - assert @connection.table_exists?("accounts") - assert !@connection.table_exists?("nonexistingtable") - assert !@connection.table_exists?(nil) + ActiveSupport::Deprecation.silence do + assert @connection.table_exists?("accounts") + assert !@connection.table_exists?("nonexistingtable") + assert !@connection.table_exists?(nil) + end + end + + def test_table_exists_checking_both_tables_and_views_is_deprecated + assert_deprecated { @connection.table_exists?("accounts") } + end + + def test_data_sources + data_sources = @connection.data_sources + assert data_sources.include?("accounts") + assert data_sources.include?("authors") + assert data_sources.include?("tasks") + assert data_sources.include?("topics") + end + + def test_data_source_exists? + assert @connection.data_source_exists?("accounts") + assert @connection.data_source_exists?(:accounts) + assert_not @connection.data_source_exists?("nonexistingtable") + assert_not @connection.data_source_exists?(nil) end def test_indexes @@ -57,13 +87,24 @@ module ActiveRecord @connection.remove_index(:accounts, :name => idx_name) rescue nil end + def test_remove_index_when_name_and_wrong_column_name_specified + index_name = "accounts_idx" + + @connection.add_index :accounts, :firm_id, :name => index_name + assert_raises ArgumentError do + @connection.remove_index :accounts, :name => index_name, :column => :wrong_column_name + end + ensure + @connection.remove_index(:accounts, :name => index_name) + end + def test_current_database if @connection.respond_to?(:current_database) assert_equal ARTest.connection_config['arunit']['database'], @connection.current_database end end - if current_adapter?(:MysqlAdapter) + if current_adapter?(:Mysql2Adapter) def test_charset assert_not_nil @connection.charset assert_not_equal 'character_set_database', @connection.charset @@ -136,14 +177,16 @@ module ActiveRecord def test_uniqueness_violations_are_translated_to_specific_exception @connection.execute "INSERT INTO subscribers(nick) VALUES('me')" - assert_raises(ActiveRecord::RecordNotUnique) do + error = assert_raises(ActiveRecord::RecordNotUnique) do @connection.execute "INSERT INTO subscribers(nick) VALUES('me')" end + + assert_not_nil error.cause end unless current_adapter?(:SQLite3Adapter) def test_foreign_key_violations_are_translated_to_specific_exception - assert_raises(ActiveRecord::InvalidForeignKey) do + error = assert_raises(ActiveRecord::InvalidForeignKey) do # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method if @connection.prefetch_primary_key? id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) @@ -152,6 +195,8 @@ module ActiveRecord @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" end end + + assert_not_nil error.cause end def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false @@ -159,11 +204,21 @@ module ActiveRecord self.table_name = 'fk_test_has_fk' end - assert_raises(ActiveRecord::InvalidForeignKey) do + error = assert_raises(ActiveRecord::InvalidForeignKey) do has_fk = klass_has_fk.new has_fk.fk_id = 1231231231 has_fk.save(validate: false) end + + assert_not_nil error.cause + end + + def test_value_limit_violations_are_translated_to_specific_exception + error = assert_raises(ActiveRecord::ValueTooLong) do + Event.create(title: 'abcdefgh') + end + + assert_not_nil error.cause end end @@ -216,13 +271,25 @@ module ActiveRecord unless current_adapter?(:PostgreSQLAdapter) def test_log_invalid_encoding - assert_raise ActiveRecord::StatementInvalid do + error = assert_raise ActiveRecord::StatementInvalid do @connection.send :log, "SELECT 'ы' FROM DUAL" do raise 'ы'.force_encoding(Encoding::ASCII_8BIT) end end + + assert_not_nil error.cause end end + + if current_adapter?(:Mysql2Adapter, :SQLite3Adapter) + def test_tables_returning_both_tables_and_views_is_deprecated + assert_deprecated { @connection.tables } + end + end + + def test_passing_arguments_to_tables_is_deprecated + assert_deprecated { @connection.tables(:books) } + end end class AdapterTestWithoutTransaction < ActiveRecord::TestCase diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb deleted file mode 100644 index 6577d56240..0000000000 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ /dev/null @@ -1,155 +0,0 @@ -require "cases/helper" -require 'support/connection_helper' - -class ActiveSchemaTest < ActiveRecord::TestCase - include ConnectionHelper - - def setup - ActiveRecord::Base.connection.singleton_class.class_eval do - alias_method :execute_without_stub, :execute - def execute(sql, name = nil) return sql end - end - end - - teardown do - reset_connection - end - - def test_add_index - # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed - def (ActiveRecord::Base.connection).table_exists?(*); true; end - 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) - - expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(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) - - 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}) - - 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}) - - %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) - 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) - 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) - - 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_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) - end - - def test_drop_table - assert_equal "DROP TABLE `people`", drop_table(:people) - end - - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) - 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}) - 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'}) - end - end - - def test_add_column - assert_equal "ALTER TABLE `people` ADD `last_name` varchar(255)", add_column(:people, :last_name, :string) - end - - def test_add_column_with_limit - 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') - 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 - 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 - end - end - end - - def test_indexes_in_create - ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false) - ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false) - - 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 - end - - assert_equal expected, actual - end - - private - def with_real_execute - ActiveRecord::Base.connection.singleton_class.class_eval do - alias_method :execute_with_stub, :execute - remove_method :execute - alias_method :execute, :execute_without_stub - end - - yield - ensure - ActiveRecord::Base.connection.singleton_class.class_eval do - remove_method :execute - alias_method :execute, :execute_with_stub - end - end - - 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/mysql/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb deleted file mode 100644 index 340fc95503..0000000000 --- a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb +++ /dev/null @@ -1,55 +0,0 @@ -require "cases/helper" -require 'models/person' - -class MysqlCaseSensitivityTest < ActiveRecord::TestCase - class CollationTest < ActiveRecord::Base - end - - 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 - end - - def test_case_sensitive - assert !CollationTest.columns_hash['string_ci_column'].case_sensitive? - assert 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') - 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') - queries = assert_sql { invalid.save } - 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') - 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') - queries = assert_sql { invalid.save } - cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } - assert_no_match(/binary/i, cs_uniqueness_query) - end -end diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb deleted file mode 100644 index 4762ef43b5..0000000000 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ /dev/null @@ -1,182 +0,0 @@ -require "cases/helper" -require 'support/connection_helper' -require 'support/ddl_helper' - -class MysqlConnectionTest < ActiveRecord::TestCase - include ConnectionHelper - include DdlHelper - - class Klass < ActiveRecord::Base - end - - def setup - super - @connection = ActiveRecord::Base.connection - end - - def test_mysql_reconnect_attribute_after_connection_with_reconnect_true - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => true})) - assert ActiveRecord::Base.connection.raw_connection.reconnect - end - end - - unless ARTest.connection_config['arunit']['socket'] - def test_connect_with_url - run_without_connection do - ar_config = ARTest.connection_config['arunit'] - - url = "mysql://#{ar_config["username"]}@localhost/#{ar_config["database"]}" - Klass.establish_connection(url) - assert_equal ar_config['database'], Klass.connection.current_database - end - end - end - - def test_mysql_reconnect_attribute_after_connection_with_reconnect_false - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => false})) - assert !ActiveRecord::Base.connection.raw_connection.reconnect - end - end - - def test_no_automatic_reconnection_after_timeout - assert @connection.active? - @connection.update('set @@wait_timeout=1') - sleep 2 - assert !@connection.active? - - # 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') - sleep 2 - @connection.reconnect! - assert @connection.active? - end - - def test_successful_reconnection_after_timeout_with_verify - assert @connection.active? - @connection.update('set @@wait_timeout=1') - sleep 2 - @connection.verify! - assert @connection.active? - end - - def test_bind_value_substitute - bind_param = @connection.substitute_at('foo') - assert_equal Arel.sql('?'), bind_param.to_sql - end - - def test_exec_no_binds - with_example_table do - result = @connection.exec_query('SELECT id, data FROM ex') - assert_equal 0, result.rows.length - assert_equal 2, result.columns.length - assert_equal %w{ id data }, result.columns - - @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') - - # if there are no bind parameters, it will return a string (due to - # the libmysql api) - result = @connection.exec_query('SELECT id, data FROM ex') - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length - - assert_equal [['1', 'foo']], result.rows - end - end - - def test_exec_with_binds - with_example_table do - @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') - result = @connection.exec_query( - 'SELECT id, data FROM ex WHERE id = ?', nil, [ActiveRecord::Relation::QueryAttribute.new("id", 1, ActiveRecord::Type::Value.new)]) - - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length - - assert_equal [[1, 'foo']], result.rows - end - end - - def test_exec_typecasts_bind_vals - with_example_table do - @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') - bind = ActiveRecord::Relation::QueryAttribute.new("id", "1-fuu", ActiveRecord::Type::Integer.new) - - result = @connection.exec_query( - 'SELECT id, data FROM ex WHERE id = ?', nil, [bind]) - - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length - - assert_equal [[1, 'foo']], result.rows - end - end - - # Test that MySQL allows multiple results for stored procedures - if defined?(Mysql) && Mysql.const_defined?(:CLIENT_MULTI_RESULTS) - def test_multi_results - rows = ActiveRecord::Base.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 'MysqlAdapter.select_rows'" - 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') - end - - def test_mysql_default_in_strict_mode - result = @connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal [["STRICT_ALL_TABLES"]], result.rows - end - - def test_mysql_strict_mode_disabled - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false})) - result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal [['']], result.rows - 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}})) - session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT" - assert_equal 3, session_mode.rows.first.first.to_i - end - end - - 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.exec_query 'SELECT @@SESSION.sql_mode' - assert_not_equal [['STRICT_ALL_TABLES']], result.rows - end - end - - 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}})) - 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 - end - end - - private - - def with_example_table(&block) - definition ||= <<-SQL - `id` int(11) auto_increment PRIMARY KEY, - `data` varchar(255) - SQL - super(@connection, 'ex', definition, &block) - end -end diff --git a/activerecord/test/cases/adapters/mysql/consistency_test.rb b/activerecord/test/cases/adapters/mysql/consistency_test.rb deleted file mode 100644 index ae190b728d..0000000000 --- a/activerecord/test/cases/adapters/mysql/consistency_test.rb +++ /dev/null @@ -1,49 +0,0 @@ -require "cases/helper" - -class MysqlConsistencyTest < ActiveRecord::TestCase - self.use_transactional_tests = false - - class Consistency < ActiveRecord::Base - self.table_name = "mysql_consistency" - end - - setup do - @old_emulate_booleans = ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans - ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false - - @connection = ActiveRecord::Base.connection - @connection.clear_cache! - @connection.create_table("mysql_consistency") do |t| - t.boolean "a_bool" - t.string "a_string" - end - Consistency.reset_column_information - Consistency.create! - end - - teardown do - ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = @old_emulate_booleans - @connection.drop_table "mysql_consistency" - end - - test "boolean columns with random value type cast to 0 when emulate_booleans is false" do - with_new = Consistency.new - with_last = Consistency.last - with_new.a_bool = 'wibble' - with_last.a_bool = 'wibble' - - assert_equal 0, with_new.a_bool - assert_equal 0, with_last.a_bool - end - - test "string columns call #to_s" do - with_new = Consistency.new - with_last = Consistency.last - thing = Object.new - with_new.a_string = thing - with_last.a_string = thing - - assert_equal thing.to_s, with_new.a_string - assert_equal thing.to_s, with_last.a_string - end -end diff --git a/activerecord/test/cases/adapters/mysql/enum_test.rb b/activerecord/test/cases/adapters/mysql/enum_test.rb deleted file mode 100644 index f4e7a3ef0a..0000000000 --- a/activerecord/test/cases/adapters/mysql/enum_test.rb +++ /dev/null @@ -1,10 +0,0 @@ -require "cases/helper" - -class MysqlEnumTest < ActiveRecord::TestCase - class EnumTest < ActiveRecord::Base - end - - def test_enum_limit - assert_equal 6, EnumTest.columns.first.limit - end -end diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb deleted file mode 100644 index 48ceef365e..0000000000 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ /dev/null @@ -1,152 +0,0 @@ - -require "cases/helper" -require 'support/ddl_helper' - -module ActiveRecord - module ConnectionAdapters - class MysqlAdapterTest < ActiveRecord::TestCase - include DdlHelper - - def setup - @conn = ActiveRecord::Base.connection - end - - def test_bad_connection_mysql - assert_raise ActiveRecord::NoDatabaseError do - configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest') - connection = ActiveRecord::Base.mysql_connection(configuration) - connection.drop_table 'ex', if_exists: true - end - end - - def test_valid_column - with_example_table do - column = @conn.columns('ex').find { |col| col.name == 'id' } - assert @conn.valid_type?(column.type) - end - end - - def test_invalid_column - assert_not @conn.valid_type?(:foobar) - end - - def test_client_encoding - assert_equal Encoding::UTF_8, @conn.client_encoding - end - - def test_exec_insert_number - with_example_table do - insert(@conn, 'number' => 10) - - result = @conn.exec_query('SELECT number FROM ex WHERE number = 10') - - assert_equal 1, result.rows.length - # if there are no bind parameters, it will return a string (due to - # the libmysql api) - assert_equal '10', result.rows.last.last - end - end - - def test_exec_insert_string - with_example_table do - str = 'いただきます!' - insert(@conn, 'number' => 10, 'data' => str) - - result = @conn.exec_query('SELECT number, data FROM ex WHERE number = 10') - - value = result.rows.last.last - - # FIXME: this should probably be inside the mysql AR adapter? - value.force_encoding(@conn.client_encoding) - - # The strings in this file are utf-8, so transcode to utf-8 - value.encode!(Encoding::UTF_8) - - assert_equal str, value - end - end - - def test_tables_quoting - @conn.tables(nil, "foo-bar", nil) - flunk - rescue => e - # assertion for *quoted* database properly - assert_match(/database 'foo-bar'/, e.inspect) - end - - def test_pk_and_sequence_for - with_example_table do - pk, seq = @conn.pk_and_sequence_for('ex') - assert_equal 'id', pk - assert_equal @conn.default_sequence_name('ex', 'id'), seq - end - end - - def test_pk_and_sequence_for_with_non_standard_primary_key - with_example_table '`code` INT(11) auto_increment, PRIMARY KEY (`code`)' do - pk, seq = @conn.pk_and_sequence_for('ex') - assert_equal 'code', pk - assert_equal @conn.default_sequence_name('ex', 'code'), seq - end - end - - def test_pk_and_sequence_for_with_custom_index_type_pk - with_example_table '`id` INT(11) auto_increment, PRIMARY KEY USING BTREE (`id`)' do - pk, seq = @conn.pk_and_sequence_for('ex') - assert_equal 'id', pk - assert_equal @conn.default_sequence_name('ex', 'id'), seq - end - end - - def test_composite_primary_key - with_example_table '`id` INT(11), `number` INT(11), foo INT(11), PRIMARY KEY (`id`, `number`)' do - assert_nil @conn.primary_key('ex') - end - end - - def test_tinyint_integer_typecasting - with_example_table '`status` TINYINT(4)' do - insert(@conn, { 'status' => 2 }, 'ex') - - result = @conn.exec_query('SELECT status FROM ex') - - assert_equal 2, result.column_types['status'].deserialize(result.last['status']) - end - end - - def test_supports_extensions - assert_not @conn.supports_extensions?, 'does not support extensions' - end - - def test_respond_to_enable_extension - assert @conn.respond_to?(:enable_extension) - end - - def test_respond_to_disable_extension - assert @conn.respond_to?(:disable_extension) - end - - private - def insert(ctx, data, table='ex') - binds = data.map { |name, value| - Relation::QueryAttribute.new(name, value, Type::Value.new) - } - columns = binds.map(&:name) - - sql = "INSERT INTO #{table} (#{columns.join(", ")}) - VALUES (#{(['?'] * columns.length).join(', ')})" - - ctx.exec_insert(sql, 'SQL', binds) - end - - def with_example_table(definition = nil, &block) - definition ||= <<-SQL - `id` int(11) auto_increment PRIMARY KEY, - `number` integer, - `data` varchar(255) - SQL - super(@conn, 'ex', definition, &block) - end - end - end -end diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb deleted file mode 100644 index a2206153e9..0000000000 --- a/activerecord/test/cases/adapters/mysql/quoting_test.rb +++ /dev/null @@ -1,21 +0,0 @@ -require "cases/helper" - -module ActiveRecord - module ConnectionAdapters - class MysqlAdapter - class QuotingTest < ActiveRecord::TestCase - def setup - @conn = ActiveRecord::Base.connection - end - - def test_type_cast_true - assert_equal 1, @conn.type_cast(true) - end - - def test_type_cast_false - assert_equal 0, @conn.type_cast(false) - end - end - end - end -end diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb deleted file mode 100644 index ec1c394f40..0000000000 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ /dev/null @@ -1,153 +0,0 @@ -require "cases/helper" - -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 - -# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with -# reserved word names (ie: group, order, values, etc...) -class MysqlReservedWordTest < ActiveRecord::TestCase - 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) } - 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/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb deleted file mode 100644 index b7f9c2ce84..0000000000 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ /dev/null @@ -1,100 +0,0 @@ -require "cases/helper" -require 'models/post' -require 'models/comment' - -module ActiveRecord - module ConnectionAdapters - class MysqlSchemaTest < ActiveRecord::TestCase - fixtures :posts - - def setup - @connection = ActiveRecord::Base.connection - db = Post.connection_pool.spec.config[:database] - table = Post.table_name - @db_name = db - - @omgpost = Class.new(ActiveRecord::Base) do - self.table_name = "#{db}.#{table}" - def self.name; 'Post'; end - end - - @connection.create_table "mysql_doubles" - end - - teardown do - @connection.drop_table "mysql_doubles", if_exists: true - end - - class MysqlDouble < ActiveRecord::Base - self.table_name = "mysql_doubles" - end - - def test_float_limits - @connection.add_column :mysql_doubles, :float_no_limit, :float - @connection.add_column :mysql_doubles, :float_short, :float, limit: 5 - @connection.add_column :mysql_doubles, :float_long, :float, limit: 53 - - @connection.add_column :mysql_doubles, :float_23, :float, limit: 23 - @connection.add_column :mysql_doubles, :float_24, :float, limit: 24 - @connection.add_column :mysql_doubles, :float_25, :float, limit: 25 - MysqlDouble.reset_column_information - - column_no_limit = MysqlDouble.columns.find { |c| c.name == 'float_no_limit' } - column_short = MysqlDouble.columns.find { |c| c.name == 'float_short' } - column_long = MysqlDouble.columns.find { |c| c.name == 'float_long' } - - column_23 = MysqlDouble.columns.find { |c| c.name == 'float_23' } - column_24 = MysqlDouble.columns.find { |c| c.name == 'float_24' } - column_25 = MysqlDouble.columns.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 - assert_equal 24, column_short.limit - assert_equal 53, column_long.limit - - assert_equal 24, column_23.limit - assert_equal 24, column_24.limit - assert_equal 53, column_25.limit - end - - def test_schema - assert @omgpost.first - end - - def test_primary_key - assert_equal 'id', @omgpost.primary_key - end - - def test_table_exists? - name = @omgpost.table_name - assert @connection.table_exists?(name), "#{name} table should exist" - end - - def test_table_exists_wrong_schema - assert(!@connection.table_exists?("#{@db_name}.zomg"), "table 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' - - table = 'key_tests' - - indexes = @connection.indexes(table).sort_by(&:name) - 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] - assert_equal :btree, index_a.using - assert_nil index_a.type - assert_equal :btree, index_b.using - assert_nil index_b.type - - assert_nil index_c.using - assert_equal :fulltext, index_c.type - end - end - end -end diff --git a/activerecord/test/cases/adapters/mysql/sp_test.rb b/activerecord/test/cases/adapters/mysql/sp_test.rb deleted file mode 100644 index 3ca2917ca4..0000000000 --- a/activerecord/test/cases/adapters/mysql/sp_test.rb +++ /dev/null @@ -1,15 +0,0 @@ -require "cases/helper" -require 'models/topic' - -class StoredProcedureTest < ActiveRecord::TestCase - fixtures :topics - - # Test that MySQL allows multiple results for stored procedures - if Mysql.const_defined?(:CLIENT_MULTI_RESULTS) - def test_multi_results_from_find_by_sql - topics = Topic.find_by_sql 'CALL topics();' - assert_equal 1, topics.size - assert ActiveRecord::Base.connection.active?, "Bad connection use by 'MysqlAdapter.select'" - end - end -end diff --git a/activerecord/test/cases/adapters/mysql/sql_types_test.rb b/activerecord/test/cases/adapters/mysql/sql_types_test.rb deleted file mode 100644 index 1ddb1b91c9..0000000000 --- a/activerecord/test/cases/adapters/mysql/sql_types_test.rb +++ /dev/null @@ -1,14 +0,0 @@ -require "cases/helper" - -class SqlTypesTest < ActiveRecord::TestCase - 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(4096)', 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) - end -end diff --git a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb deleted file mode 100644 index 209a0cf464..0000000000 --- a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb +++ /dev/null @@ -1,23 +0,0 @@ -require 'cases/helper' - -module ActiveRecord::ConnectionAdapters - class MysqlAdapter - class StatementPoolTest < ActiveRecord::TestCase - if Process.respond_to?(:fork) - def test_cache_is_per_pid - cache = StatementPool.new nil, 10 - cache['foo'] = 'bar' - assert_equal 'bar', cache['foo'] - - pid = fork { - lookup = cache['foo']; - exit!(!lookup) - } - - Process.waitpid pid - assert $?.success?, 'process should exit successfully' - end - end - end - end -end diff --git a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb deleted file mode 100644 index e9edc53f93..0000000000 --- a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb +++ /dev/null @@ -1,30 +0,0 @@ -require "cases/helper" - -class UnsignedTypeTest < ActiveRecord::TestCase - self.use_transactional_tests = false - - class UnsignedType < ActiveRecord::Base - end - - setup do - @connection = ActiveRecord::Base.connection - @connection.create_table("unsigned_types", force: true) do |t| - t.column :unsigned_integer, "int unsigned" - end - end - - teardown do - @connection.drop_table "unsigned_types" - end - - test "unsigned int max value is in range" do - assert expected = UnsignedType.create(unsigned_integer: 4294967295) - assert_equal expected, UnsignedType.find_by(unsigned_integer: 4294967295) - end - - test "minus value is out of range" do - assert_raise(RangeError) do - UnsignedType.create(unsigned_integer: -10) - end - end -end diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index e87cd3886a..95d1f6b8a3 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/connection_helper' -class ActiveSchemaTest < ActiveRecord::TestCase +class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase include ConnectionHelper def setup @@ -16,8 +16,8 @@ class ActiveSchemaTest < ActiveRecord::TestCase end def test_add_index - # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed - def (ActiveRecord::Base.connection).table_exists?(*); true; end + # add_index calls data_source_exists? and index_name_exists? which can't work since execute is stubbed + def (ActiveRecord::Base.connection).data_source_exists?(*); true; end def (ActiveRecord::Base.connection).index_name_exists?(*); false; end expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) " @@ -59,21 +59,56 @@ class ActiveSchemaTest < ActiveRecord::TestCase assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree) end - def test_drop_table - assert_equal "DROP TABLE `people`", drop_table(:people) + 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" + actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| + t.index :last_name, type: type + end + assert_equal expected, actual + end + + expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10))) ENGINE=InnoDB" + actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| + t.index :last_name, length: 10, using: :btree + end + assert_equal expected, actual end - if current_adapter?(:Mysql2Adapter) - 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}) + def test_index_in_bulk_change + def (ActiveRecord::Base.connection).data_source_exists?(*); true; end + def (ActiveRecord::Base.connection).index_name_exists?(*); false; end + + %w(SPATIAL FULLTEXT UNIQUE).each do |type| + expected = "ALTER TABLE `people` ADD #{type} INDEX `index_people_on_last_name` (`last_name`)" + actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| + t.index :last_name, type: type + end + assert_equal expected, actual 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'}) + 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| + t.index :last_name, length: 10, using: :btree, algorithm: :copy end + assert_equal expected, actual + end + + def test_drop_table + assert_equal "DROP TABLE `people`", drop_table(:people) + 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}) + 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'}) end def test_add_column @@ -117,10 +152,10 @@ class ActiveSchemaTest < ActiveRecord::TestCase end def test_indexes_in_create - ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false) + 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) - expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query" + 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 end diff --git a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb index 5e8065d80d..abdf3dbf5b 100644 --- a/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/bind_parameter_test.rb @@ -4,7 +4,7 @@ require 'models/topic' module ActiveRecord module ConnectionAdapters class Mysql2Adapter - class BindParameterTest < ActiveRecord::TestCase + class BindParameterTest < ActiveRecord::Mysql2TestCase fixtures :topics def test_update_question_marks diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 0d81dd6eee..739bb275ce 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class Mysql2BooleanTest < ActiveRecord::TestCase +class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase self.use_transactional_tests = false class BooleanType < ActiveRecord::Base @@ -43,11 +43,16 @@ class Mysql2BooleanTest < ActiveRecord::TestCase 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 @@ -55,11 +60,16 @@ class Mysql2BooleanTest < ActiveRecord::TestCase 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 diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb index 09bebf3071..9cb05119a2 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -1,7 +1,6 @@ require "cases/helper" -require 'models/person' -class Mysql2CaseSensitivityTest < ActiveRecord::TestCase +class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase class CollationTest < ActiveRecord::Base end @@ -52,4 +51,13 @@ class Mysql2CaseSensitivityTest < ActiveRecord::TestCase cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } assert_no_match(/binary/i, cs_uniqueness_query) end + + 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') + queries = assert_sql { invalid.save } + bin_uniqueness_query = queries.detect { |q| q.match(/binary_column/) } + assert_no_match(/\bBINARY\b/, bin_uniqueness_query) + 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..668c07dacb --- /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 Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + self.use_transactional_tests = 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+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/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index a8b39b21d4..fe610ae951 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/connection_helper' -class MysqlConnectionTest < ActiveRecord::TestCase +class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase include ConnectionHelper fixtures :comments @@ -68,35 +68,55 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert_equal 'utf8_general_ci', ARUnit2Model.connection.show_variable('collation_connection') end - # TODO: Below is a straight up copy/paste from mysql/connection_test.rb - # I'm not sure what the correct way is to share these tests between - # adapters in minitest. def test_mysql_default_in_strict_mode - result = @connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal [["STRICT_ALL_TABLES"]], result.rows + result = @connection.select_value("SELECT @@SESSION.sql_mode") + assert_match %r(STRICT_ALL_TABLES), result end def test_mysql_strict_mode_disabled run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false})) - result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal [['']], result.rows + ActiveRecord::Base.establish_connection(orig_connection.merge(strict: false)) + result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.sql_mode") + assert_no_match %r(STRICT_ALL_TABLES), result end end - def test_mysql_set_session_variable + def test_mysql_strict_mode_specified_default run_without_connection do |orig_connection| - 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 + ActiveRecord::Base.establish_connection(orig_connection.merge(strict: :default)) + global_sql_mode = ActiveRecord::Base.connection.select_value("SELECT @@GLOBAL.sql_mode") + session_sql_mode = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.sql_mode") + assert_equal global_sql_mode, session_sql_mode end end 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.exec_query 'SELECT @@SESSION.sql_mode' - assert_not_equal [['STRICT_ALL_TABLES']], result.rows + 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 + 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] + 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'] })) + 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}})) + session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT" + assert_equal 3, session_mode.rows.first.first.to_i end end @@ -122,4 +142,32 @@ class MysqlConnectionTest < ActiveRecord::TestCase ensure @connection.execute "DROP TABLE `bar_baz`" end + + def test_get_and_release_advisory_lock + 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" + + assert_equal test_lock_free(lock_name), false, + "expected the test advisory lock to be held but it wasn't" + + 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' + end + + def test_release_non_existent_advisory_lock + 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' + end + + protected + + 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..e349c67c93 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb @@ -0,0 +1,45 @@ +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' + assert_microsecond_precision + end + + test 'no microsecond precision for MySQL lt 5.6.4' do + stub_version '5.6.3' + assert_no_microsecond_precision + end + + test 'microsecond precision for MariaDB gte 5.3.0' do + stub_version '5.5.5-10.1.8-MariaDB-log' + assert_microsecond_precision + end + + test 'no microsecond precision for MariaDB lt 5.3.0' do + stub_version '5.2.9-MariaDB' + assert_no_microsecond_precision + 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.stubs(:full_version).returns(full_version_string) + @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 6dd9a5ec87..35dbc76d1b 100644 --- a/activerecord/test/cases/adapters/mysql2/enum_test.rb +++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb @@ -1,10 +1,26 @@ require "cases/helper" -class Mysql2EnumTest < ActiveRecord::TestCase +class Mysql2EnumTest < ActiveRecord::Mysql2TestCase class EnumTest < ActiveRecord::Base end def test_enum_limit - assert_equal 6, EnumTest.columns.first.limit + 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? + end + + def test_should_not_be_bigint + column = EnumTest.columns_hash['enum_column'] + assert_not 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 2b01d941b8..b783b5fcd9 100644 --- a/activerecord/test/cases/adapters/mysql2/explain_test.rb +++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb @@ -2,26 +2,20 @@ require "cases/helper" require 'models/developer' require 'models/computer' -module ActiveRecord - module ConnectionAdapters - class Mysql2Adapter - class ExplainTest < ActiveRecord::TestCase - fixtures :developers +class Mysql2ExplainTest < ActiveRecord::Mysql2TestCase + fixtures :developers - 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 - end + 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 + 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 - end - end - 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 end end diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb new file mode 100644 index 0000000000..9c3fef1b59 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/json_test.rb @@ -0,0 +1,179 @@ +require 'cases/helper' +require 'support/schema_dumping_helper' + +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 + + def setup + @connection = ActiveRecord::Base.connection + begin + @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_string_literal + json = JsonDataType.create(payload: "foo") + assert_equal "foo", json.payload + end + + def test_assigning_number + json = JsonDataType.create(payload: 1.234) + assert_equal 1.234, json.payload + end + + def test_assigning_boolean + json = JsonDataType.create(payload: true) + assert_equal true, json.payload + 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 new file mode 100644 index 0000000000..61dd0828d0 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb @@ -0,0 +1,73 @@ +require "cases/helper" +require "support/ddl_helper" + +class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase + include DdlHelper + + def setup + @conn = ActiveRecord::Base.connection + end + + 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') + end + end + end + + def test_valid_column + with_example_table do + column = @conn.columns('ex').find { |col| col.name == 'id' } + assert @conn.valid_type?(column.type) + end + end + + def test_invalid_column + assert_not @conn.valid_type?(:foobar) + end + + def test_columns_for_distinct_zero_orders + assert_equal "posts.id", + @conn.columns_for_distinct("posts.id", []) + end + + def test_columns_for_distinct_one_order + assert_equal "posts.id, posts.created_at AS alias_0", + @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", + @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"]) + ) + end + + def test_columns_for_distinct_blank_not_nil_orders + assert_equal "posts.id, posts.created_at AS alias_0", + @conn.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "]) + end + + def test_columns_for_distinct_with_arel_order + order = Object.new + def order.to_sql + "posts.created_at desc" + end + assert_equal "posts.id, posts.created_at AS alias_0", + @conn.columns_for_distinct("posts.id", [order]) + 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/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 799e60a683..ffb4e2c5cf 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -1,29 +1,29 @@ require "cases/helper" -class Group < ActiveRecord::Base - Group.table_name = 'group' - belongs_to :select - has_one :values -end +# 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 Select < ActiveRecord::Base + Select.table_name = 'select' + has_many :groups + end -class Values < ActiveRecord::Base - Values.table_name = 'values' -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 + class Distinct < ActiveRecord::Base + Distinct.table_name = 'distinct' + has_and_belongs_to_many :selects + has_many :values, :through => :groups + end -# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with -# reserved word names (ie: group, order, values, etc...) -class MysqlReservedWordTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index 417ccf6d11..7c89fda582 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -1,47 +1,59 @@ require "cases/helper" -module ActiveRecord - module ConnectionAdapters - class AbstractMysqlAdapter - class SchemaMigrationsTest < ActiveRecord::TestCase - def test_renaming_index_on_foreign_key - connection.add_index "engines", "car_id" - connection.add_foreign_key :engines, :cars, name: "fk_engines_cars" - - connection.rename_index("engines", "index_engines_on_car_id", "idx_renamed") - assert_equal ["idx_renamed"], connection.indexes("engines").map(&:name) - ensure - connection.remove_foreign_key :engines, name: "fk_engines_cars" - end - - def test_initializes_schema_migrations_for_encoding_utf8mb4 - smtn = ActiveRecord::Migrator.schema_migrations_table_name - connection.drop_table smtn, if_exists: true - - 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"] - - execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET utf8mb4") - - connection.initialize_schema_migrations_table - - assert connection.column_exists?(smtn, :version, :string, limit: AbstractMysqlAdapter::MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN) - ensure - execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}") - end - - private - def connection - @connection ||= ActiveRecord::Base.connection - end - - def execute(sql) - connection.execute(sql) - end - end +class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase + def test_renaming_index_on_foreign_key + connection.add_index "engines", "car_id" + connection.add_foreign_key :engines, :cars, name: "fk_engines_cars" + + connection.rename_index("engines", "index_engines_on_car_id", "idx_renamed") + assert_equal ["idx_renamed"], connection.indexes("engines").map(&:name) + ensure + connection.remove_foreign_key :engines, name: "fk_engines_cars" + end + + def test_initializes_schema_migrations_for_encoding_utf8mb4 + with_encoding_utf8mb4 do + table_name = ActiveRecord::SchemaMigration.table_name + connection.drop_table table_name, if_exists: true + + connection.initialize_schema_migrations_table + + assert connection.column_exists?(table_name, :version, :string, collation: 'utf8_general_ci') + end + end + + def test_initializes_internal_metadata_for_encoding_utf8mb4 + with_encoding_utf8mb4 do + table_name = ActiveRecord::InternalMetadata.table_name + connection.drop_table table_name, if_exists: true + + connection.initialize_internal_metadata_table + + assert connection.column_exists?(table_name, :key, :string, collation: 'utf8_general_ci') end 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}'") + + original_charset = database_info["DEFAULT_CHARACTER_SET_NAME"] + original_collation = database_info["DEFAULT_COLLATION_NAME"] + + 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 + + def connection + @connection ||= ActiveRecord::Base.connection + 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 47707b7d4f..43957791b1 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -4,7 +4,7 @@ require 'models/comment' module ActiveRecord module ConnectionAdapters - class Mysql2SchemaTest < ActiveRecord::TestCase + class Mysql2SchemaTest < ActiveRecord::Mysql2TestCase fixtures :posts def setup @@ -14,11 +14,43 @@ module ActiveRecord @db_name = db @omgpost = Class.new(ActiveRecord::Base) do + self.inheritance_column = :disabled self.table_name = "#{db}.#{table}" def self.name; 'Post'; end end end + def test_float_limits + @connection.create_table :mysql_doubles do |t| + t.float :float_no_limit + t.float :float_short, limit: 5 + t.float :float_long, limit: 53 + + t.float :float_23, limit: 23 + t.float :float_24, limit: 24 + 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_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 + assert_equal 24, column_short.limit + assert_equal 53, column_long.limit + + assert_equal 24, column_23.limit + assert_equal 24, column_24.limit + assert_equal 53, column_25.limit + ensure + @connection.drop_table "mysql_doubles", if_exists: true + end + def test_schema assert @omgpost.first end @@ -27,21 +59,13 @@ module ActiveRecord assert_equal 'id', @omgpost.primary_key end - def test_table_exists? + def test_data_source_exists? name = @omgpost.table_name - assert @connection.table_exists?(name), "#{name} table should exist" - end - - def test_table_exists_wrong_schema - assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist") + assert @connection.data_source_exists?(name), "#{name} data_source should exist" end - def test_tables_quoting - @connection.tables(nil, "foo-bar", nil) - flunk - rescue => e - # assertion for *quoted* database properly - assert_match(/database 'foo-bar'/, e.inspect) + def test_data_source_exists_wrong_schema + assert(!@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist") end def test_dump_indexes diff --git a/activerecord/test/cases/adapters/mysql2/sp_test.rb b/activerecord/test/cases/adapters/mysql2/sp_test.rb new file mode 100644 index 0000000000..4197ba45f1 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/sp_test.rb @@ -0,0 +1,36 @@ +require "cases/helper" +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' + skip("no stored procedure support") + end + end + + # 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 + def test_multi_results + 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'] + 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);' + assert_equal 3, topics.size + assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select'" + end +end diff --git a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb index 1ddb1b91c9..4926bc2267 100644 --- a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb +++ b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb @@ -1,10 +1,10 @@ require "cases/helper" -class SqlTypesTest < ActiveRecord::TestCase +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(4096)', type_to_sql(:binary, 4096) + assert_equal 'blob', type_to_sql(:binary, 4096) assert_equal 'blob', type_to_sql(:binary) end diff --git a/activerecord/test/cases/adapters/mysql2/table_options_test.rb b/activerecord/test/cases/adapters/mysql2/table_options_test.rb new file mode 100644 index 0000000000..af121ee7d9 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/table_options_test.rb @@ -0,0 +1,42 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class Mysql2TableOptionsTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + @connection.drop_table "mysql_table_options", if_exists: true + end + + 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] + 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] + 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] + 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] + assert_match %r{COLLATE=utf8mb4_bin}, options + 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 e9edc53f93..3df11ce11b 100644 --- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -1,6 +1,8 @@ require "cases/helper" +require "support/schema_dumping_helper" -class UnsignedTypeTest < ActiveRecord::TestCase +class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper self.use_transactional_tests = false class UnsignedType < ActiveRecord::Base @@ -9,12 +11,15 @@ class UnsignedTypeTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection @connection.create_table("unsigned_types", force: true) do |t| - t.column :unsigned_integer, "int unsigned" + t.integer :unsigned_integer, unsigned: true + t.bigint :unsigned_bigint, unsigned: true + t.float :unsigned_float, unsigned: true + t.decimal :unsigned_decimal, unsigned: true, precision: 10, scale: 2 end end teardown do - @connection.drop_table "unsigned_types" + @connection.drop_table "unsigned_types", if_exists: true end test "unsigned int max value is in range" do @@ -23,8 +28,38 @@ class UnsignedTypeTest < ActiveRecord::TestCase 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(ActiveModel::RangeError) do + UnsignedType.create(unsigned_bigint: -10) + end + assert_raise(ActiveRecord::StatementInvalid) do + UnsignedType.create(unsigned_float: -10.0) + end + assert_raise(ActiveRecord::StatementInvalid) do + UnsignedType.create(unsigned_decimal: -10.0) + end + end + + test "schema definition can use unsigned as the type" do + @connection.change_table("unsigned_types") do |t| + t.unsigned_integer :unsigned_integer_t + t.unsigned_bigint :unsigned_bigint_t + t.unsigned_float :unsigned_float_t + 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? + 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 end end diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index 3808db5141..439e2ce6f7 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -1,6 +1,6 @@ require 'cases/helper' -class PostgresqlActiveSchemaTest < ActiveRecord::TestCase +class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase def setup ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do def execute(sql, name = nil) sql end @@ -25,10 +25,16 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase def test_add_index # add_index calls index_name_exists? which can't work since execute is stubbed - ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.stubs(:index_name_exists?).returns(false) + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) { |*| false } expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" ("last_name") WHERE state = 'active') - assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'") + assert_equal expected, add_index(:people, :last_name, unique: true, where: "state = 'active'") + + expected = %(CREATE UNIQUE INDEX "index_people_on_lower_last_name" ON "people" (lower(last_name))) + assert_equal expected, add_index(:people, 'lower(last_name)', unique: true) + + expected = %(CREATE UNIQUE INDEX "index_people_on_last_name_varchar_pattern_ops" ON "people" (last_name varchar_pattern_ops)) + assert_equal expected, add_index(:people, 'last_name varchar_pattern_ops', unique: true) expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" ("last_name")) assert_equal expected, add_index(:people, :last_name, algorithm: :concurrently) @@ -39,16 +45,40 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" USING #{type} ("last_name")) assert_equal expected, add_index(:people, :last_name, using: type, algorithm: :concurrently) + + expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING #{type} ("last_name") WHERE state = 'active') + assert_equal expected, add_index(:people, :last_name, using: type, unique: true, where: "state = 'active'") + + expected = %(CREATE UNIQUE INDEX "index_people_on_lower_last_name" ON "people" USING #{type} (lower(last_name))) + assert_equal expected, add_index(:people, 'lower(last_name)', using: type, unique: true) end assert_raise ArgumentError do add_index(:people, :last_name, algorithm: :copy) end - expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name")) - assert_equal expected, add_index(:people, :last_name, :unique => true, :using => :gist) - expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name") WHERE state = 'active') - assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'", :using => :gist) + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_exists? + end + + def test_remove_index + # remove_index calls index_name_for_remove which can't work since execute is stubbed + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_for_remove) do |*| + 'index_people_on_last_name' + end + + expected = %(DROP INDEX CONCURRENTLY "index_people_on_last_name") + assert_equal expected, remove_index(:people, name: "index_people_on_last_name", algorithm: :concurrently) + + assert_raise ArgumentError do + add_index(:people, :last_name, algorithm: :copy) + end + + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_for_remove + end + + def test_remove_index_when_name_is_specified + expected = %(DROP INDEX CONCURRENTLY "index_people_on_last_name") + assert_equal expected, remove_index(:people, name: "index_people_on_last_name", algorithm: :concurrently) end private diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 6edbd9c3a6..380a90d765 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -1,10 +1,9 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlArrayTest < ActiveRecord::TestCase +class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper include InTimeZone - OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID class PgArray < ActiveRecord::Base self.table_name = 'pg_arrays' @@ -212,8 +211,9 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def test_quoting_non_standard_delimiters strings = ["hello,", "world;"] - comma_delim = OID::Array.new(ActiveRecord::Type::String.new, ',') - semicolon_delim = OID::Array.new(ActiveRecord::Type::String.new, ';') + oid = ActiveRecord::ConnectionAdapters::PostgreSQL::OID + comma_delim = oid::Array.new(ActiveRecord::Type::String.new, ',') + semicolon_delim = oid::Array.new(ActiveRecord::Type::String.new, ';') assert_equal %({"hello,",world;}), comma_delim.serialize(strings) assert_equal %({hello,;"world;"}), semicolon_delim.serialize(strings) diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb index 1a5ff4316c..cec6081aec 100644 --- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -2,7 +2,7 @@ require "cases/helper" require 'support/connection_helper' require 'support/schema_dumping_helper' -class PostgresqlBitStringTest < ActiveRecord::TestCase +class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper include SchemaDumpingHelper @@ -57,9 +57,11 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase assert_match %r{t\.bit_varying\s+"a_bit_varying",\s+limit: 4,\s+default: "0011"$}, output end - def test_assigning_invalid_hex_string_raises_exception - assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit: "FF" } - assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit_varying: "FF" } + if ActiveRecord::Base.connection.prepared_statements + def test_assigning_invalid_hex_string_raises_exception + assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit: "FF" } + assert_raises(ActiveRecord::StatementInvalid) { PostgresqlBitString.create! a_bit_varying: "F" } + end end def test_roundtrip diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index 16db5ab83d..7adc070430 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -1,6 +1,9 @@ require "cases/helper" +require 'support/schema_dumping_helper' + +class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper -class PostgresqlByteaTest < ActiveRecord::TestCase class ByteaDataType < ActiveRecord::Base self.table_name = 'bytea_data_type' end @@ -122,4 +125,10 @@ class PostgresqlByteaTest < ActiveRecord::TestCase obj.reload assert_equal "hello world", obj.serialized end + + def test_schema_dumping + output = dump_table_schema("bytea_data_type") + assert_match %r{t\.binary\s+"payload"$}, output + assert_match %r{t\.binary\s+"serialized"$}, output + end end diff --git a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb index 5a9796887c..bc12df668d 100644 --- a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb @@ -2,7 +2,7 @@ require 'cases/helper' module ActiveRecord class Migration - class PGChangeSchemaTest < ActiveRecord::TestCase + class PGChangeSchemaTest < ActiveRecord::PostgreSQLTestCase attr_reader :connection def setup diff --git a/activerecord/test/cases/adapters/postgresql/cidr_test.rb b/activerecord/test/cases/adapters/postgresql/cidr_test.rb index 6cb11d17b4..52f2a0096c 100644 --- a/activerecord/test/cases/adapters/postgresql/cidr_test.rb +++ b/activerecord/test/cases/adapters/postgresql/cidr_test.rb @@ -3,8 +3,8 @@ require "ipaddr" module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapter - class CidrTest < ActiveRecord::TestCase + class PostgreSQLAdapter < AbstractAdapter + class CidrTest < ActiveRecord::PostgreSQLTestCase test "type casting IPAddr for database" do type = OID::Cidr.new ip = IPAddr.new("255.0.0.0/8") diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb index f706847890..bd62041e79 100644 --- a/activerecord/test/cases/adapters/postgresql/citext_test.rb +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -2,7 +2,7 @@ require 'cases/helper' require 'support/schema_dumping_helper' if ActiveRecord::Base.connection.supports_extensions? - class PostgresqlCitextTest < ActiveRecord::TestCase + class PostgresqlCitextTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class Citext < ActiveRecord::Base self.table_name = 'citexts' diff --git a/activerecord/test/cases/adapters/postgresql/collation_test.rb b/activerecord/test/cases/adapters/postgresql/collation_test.rb new file mode 100644 index 0000000000..8470329c35 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/collation_test.rb @@ -0,0 +1,53 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class PostgresqlCollationTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :postgresql_collations, force: true do |t| + t.string :string_c, collation: 'C' + t.text :text_posix, collation: 'POSIX' + end + end + + def teardown + @connection.drop_table :postgresql_collations, if_exists: true + end + + test "string column with collation" do + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'string_c' } + assert_equal :string, column.type + assert_equal 'C', column.collation + end + + test "text column with collation" do + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'text_posix' } + assert_equal :text, column.type + assert_equal 'POSIX', column.collation + end + + test "add column with collation" do + @connection.add_column :postgresql_collations, :title, :string, collation: 'C' + + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'C', column.collation + end + + test "change column with collation" do + @connection.add_column :postgresql_collations, :description, :string + @connection.change_column :postgresql_collations, :description, :text, collation: 'POSIX' + + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'POSIX', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("postgresql_collations") + assert_match %r{t.string\s+"string_c",\s+collation: "C"$}, output + assert_match %r{t.text\s+"text_posix",\s+collation: "POSIX"$}, output + end +end diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 16e3f90a47..1de87e5f01 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -40,7 +40,7 @@ end # "unknown OID 5653508: failed to recognize type of 'address'. It will be treated as String." # To take full advantage of composite types, we suggest you register your own +OID::Type+. # See PostgresqlCompositeWithCustomOIDTest -class PostgresqlCompositeTest < ActiveRecord::TestCase +class PostgresqlCompositeTest < ActiveRecord::PostgreSQLTestCase include PostgresqlCompositeBehavior def test_column @@ -77,7 +77,7 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase end end -class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase +class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::PostgreSQLTestCase include PostgresqlCompositeBehavior class FullAddressType < ActiveRecord::Type::Value diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 55ad76c8c0..f8403bfe1a 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -2,7 +2,7 @@ require "cases/helper" require 'support/connection_helper' module ActiveRecord - class PostgresqlConnectionTest < ActiveRecord::TestCase + class PostgresqlConnectionTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper class NonExistentTable < ActiveRecord::Base @@ -90,7 +90,7 @@ module ActiveRecord end def test_tables_logs_name - @connection.tables('hello') + ActiveSupport::Deprecation.silence { @connection.tables('hello') } assert_equal 'SCHEMA', @subscriber.logged[0][1] end @@ -100,7 +100,7 @@ module ActiveRecord end def test_table_exists_logs_name - @connection.table_exists?('items') + ActiveSupport::Deprecation.silence { @connection.table_exists?('items') } assert_equal 'SCHEMA', @subscriber.logged[0][1] end @@ -125,14 +125,16 @@ module ActiveRecord assert_equal 'SCHEMA', @subscriber.logged[0][1] end - def test_statement_key_is_logged - bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new) - @connection.exec_query('SELECT $1::integer', 'SQL', [bind]) - name = @subscriber.payloads.last[:statement_name] - assert name - res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)") - plan = res.column_types['QUERY PLAN'].deserialize res.rows.first.first - assert_operator plan.length, :>, 0 + if ActiveRecord::Base.connection.prepared_statements + def test_statement_key_is_logged + bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new) + @connection.exec_query('SELECT $1::integer', 'SQL', [bind], prepare: true) + name = @subscriber.payloads.last[:statement_name] + assert name + res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)") + plan = res.column_types['QUERY PLAN'].deserialize res.rows.first.first + assert_operator plan.length, :>, 0 + end end # Must have PostgreSQL >= 9.2, or with_manual_interventions set to @@ -209,5 +211,47 @@ module ActiveRecord ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => :default}})) end end + + def test_get_and_release_advisory_lock + lock_id = 5295901941911233559 + list_advisory_locks = <<-SQL + SELECT locktype, + (classid::bigint << 32) | objid::bigint AS lock_id + FROM pg_locks + WHERE locktype = 'advisory' + SQL + + got_lock = @connection.get_advisory_lock(lock_id) + assert got_lock, "get_advisory_lock should have returned true but it didn't" + + advisory_lock = @connection.query(list_advisory_locks).find {|l| l[1] == lock_id} + assert advisory_lock, + "expected to find an advisory lock with lock_id #{lock_id} but there wasn't one" + + released_lock = @connection.release_advisory_lock(lock_id) + assert released_lock, "expected release_advisory_lock to return true but it didn't" + + advisory_locks = @connection.query(list_advisory_locks).select {|l| l[1] == lock_id} + assert_empty advisory_locks, + "expected to have released advisory lock with lock_id #{lock_id} but it was still held" + end + + def test_release_non_existent_advisory_lock + fake_lock_id = 2940075057017742022 + with_warning_suppression do + released_non_existent_lock = @connection.release_advisory_lock(fake_lock_id) + assert_equal released_non_existent_lock, false, + 'expected release_advisory_lock to return false when there was no lock to release' + end + end + + protected + + def with_warning_suppression + log_level = @connection.client_min_messages + @connection.client_min_messages = 'error' + yield + @connection.client_min_messages = log_level + end end end diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 2c14252ae4..232c25cb3b 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -11,7 +11,7 @@ end class PostgresqlLtree < ActiveRecord::Base end -class PostgresqlDataTypeTest < ActiveRecord::TestCase +class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false def setup @@ -69,7 +69,7 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end end -class PostgresqlInternalDataTypeTest < ActiveRecord::TestCase +class PostgresqlInternalDataTypeTest < ActiveRecord::PostgreSQLTestCase include DdlHelper setup do diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb index 26e064c937..6102ddacd1 100644 --- a/activerecord/test/cases/adapters/postgresql/domain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/connection_helper' -class PostgresqlDomainTest < ActiveRecord::TestCase +class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper class PostgresqlDomain < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index ed084483bc..6816a6514b 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/connection_helper' -class PostgresqlEnumTest < ActiveRecord::TestCase +class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper class PostgresqlEnum < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 6ffb4c9f33..29bf2c15ea 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -2,25 +2,19 @@ require "cases/helper" require 'models/developer' require 'models/computer' -module ActiveRecord - module ConnectionAdapters - class PostgreSQLAdapter - class ExplainTest < ActiveRecord::TestCase - fixtures :developers +class PostgreSQLExplainTest < ActiveRecord::PostgreSQLTestCase + fixtures :developers - 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 %(QUERY PLAN), explain - end + def test_explain_for_one_query + explain = Developer.where(id: 1).explain + assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = \$?1), explain + assert_match %(QUERY PLAN), explain + end - def test_explain_with_eager_loading - explain = Developer.where(:id => 1).includes(:audit_logs).explain - assert_match %(QUERY PLAN), explain - assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain - end - end - end + def test_explain_with_eager_loading + explain = Developer.where(id: 1).includes(:audit_logs).explain + assert_match %(QUERY PLAN), explain + assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = \$?1), explain + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain end end diff --git a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb index 06d427f464..b56c226763 100644 --- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb +++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb @@ -1,15 +1,15 @@ require "cases/helper" -class PostgresqlExtensionMigrationTest < ActiveRecord::TestCase +class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false - class EnableHstore < ActiveRecord::Migration + class EnableHstore < ActiveRecord::Migration::Current def change enable_extension "hstore" end end - class DisableHstore < ActiveRecord::Migration + class DisableHstore < ActiveRecord::Migration::Current def change disable_extension "hstore" end @@ -24,9 +24,9 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::TestCase return skip("no extension support") end - @old_schema_migration_tabel_name = ActiveRecord::SchemaMigration.table_name - @old_tabel_name_prefix = ActiveRecord::Base.table_name_prefix - @old_tabel_name_suffix = ActiveRecord::Base.table_name_suffix + @old_schema_migration_table_name = ActiveRecord::SchemaMigration.table_name + @old_table_name_prefix = ActiveRecord::Base.table_name_prefix + @old_table_name_suffix = ActiveRecord::Base.table_name_suffix ActiveRecord::Base.table_name_prefix = "p_" ActiveRecord::Base.table_name_suffix = "_s" @@ -36,11 +36,11 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::TestCase end def teardown - ActiveRecord::Base.table_name_prefix = @old_tabel_name_prefix - ActiveRecord::Base.table_name_suffix = @old_tabel_name_suffix + ActiveRecord::Base.table_name_prefix = @old_table_name_prefix + ActiveRecord::Base.table_name_suffix = @old_table_name_suffix ActiveRecord::SchemaMigration.delete_all rescue nil ActiveRecord::Migration.verbose = true - ActiveRecord::SchemaMigration.table_name = @old_schema_migration_tabel_name + ActiveRecord::SchemaMigration.table_name = @old_schema_migration_table_name super end diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb index b83063c94e..bde7513339 100644 --- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlFullTextTest < ActiveRecord::TestCase +class PostgresqlFullTextTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class Tsvector < ActiveRecord::Base; end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index 41e9572907..66f0a70394 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -2,11 +2,19 @@ require "cases/helper" require 'support/connection_helper' require 'support/schema_dumping_helper' -class PostgresqlPointTest < ActiveRecord::TestCase +class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase include ConnectionHelper include SchemaDumpingHelper - class PostgresqlPoint < ActiveRecord::Base; end + class PostgresqlPoint < ActiveRecord::Base + attribute :x, :point + attribute :y, :point + attribute :z, :point + attribute :array_of_points, :point, array: true + attribute :legacy_x, :legacy_point + attribute :legacy_y, :legacy_point + attribute :legacy_z, :legacy_point + end def setup @connection = ActiveRecord::Base.connection @@ -14,11 +22,27 @@ class PostgresqlPointTest < ActiveRecord::TestCase t.point :x t.point :y, default: [12.2, 13.3] t.point :z, default: "(14.4,15.5)" + t.point :array_of_points, array: true + t.point :legacy_x + t.point :legacy_y, default: [12.2, 13.3] + t.point :legacy_z, default: "(14.4,15.5)" + end + @connection.create_table('deprecated_points') do |t| + t.point :x end end teardown do @connection.drop_table 'postgresql_points', if_exists: true + @connection.drop_table 'deprecated_points', if_exists: true + end + + class DeprecatedPoint < ActiveRecord::Base; end + + def test_deprecated_legacy_type + assert_deprecated do + DeprecatedPoint.new + end end def test_column @@ -32,11 +56,11 @@ class PostgresqlPointTest < ActiveRecord::TestCase end def test_default - assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['y'] - assert_equal [12.2, 13.3], PostgresqlPoint.new.y + assert_equal ActiveRecord::Point.new(12.2, 13.3), PostgresqlPoint.column_defaults['y'] + assert_equal ActiveRecord::Point.new(12.2, 13.3), PostgresqlPoint.new.y - assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['z'] - assert_equal [14.4, 15.5], PostgresqlPoint.new.z + assert_equal ActiveRecord::Point.new(14.4, 15.5), PostgresqlPoint.column_defaults['z'] + assert_equal ActiveRecord::Point.new(14.4, 15.5), PostgresqlPoint.new.z end def test_schema_dumping @@ -49,37 +73,119 @@ class PostgresqlPointTest < ActiveRecord::TestCase def test_roundtrip PostgresqlPoint.create! x: [10, 25.2] record = PostgresqlPoint.first - assert_equal [10, 25.2], record.x + assert_equal ActiveRecord::Point.new(10, 25.2), record.x - record.x = [1.1, 2.2] + record.x = ActiveRecord::Point.new(1.1, 2.2) record.save! assert record.reload - assert_equal [1.1, 2.2], record.x + assert_equal ActiveRecord::Point.new(1.1, 2.2), record.x end def test_mutation - p = PostgresqlPoint.create! x: [10, 20] + p = PostgresqlPoint.create! x: ActiveRecord::Point.new(10, 20) - p.x[1] = 25 + p.x.y = 25 p.save! p.reload - assert_equal [10.0, 25.0], p.x + assert_equal ActiveRecord::Point.new(10.0, 25.0), p.x + assert_not p.changed? + end + + def test_array_assignment + p = PostgresqlPoint.new(x: [1, 2]) + + assert_equal ActiveRecord::Point.new(1, 2), p.x + end + + def test_string_assignment + p = PostgresqlPoint.new(x: "(1, 2)") + + assert_equal ActiveRecord::Point.new(1, 2), p.x + end + + def test_empty_string_assignment + assert_nothing_raised { PostgresqlPoint.new(x: "") } + + p = PostgresqlPoint.new(x: "") + assert_equal nil, p.x + end + + def test_array_of_points_round_trip + expected_value = [ + ActiveRecord::Point.new(1, 2), + ActiveRecord::Point.new(2, 3), + ActiveRecord::Point.new(3, 4), + ] + p = PostgresqlPoint.new(array_of_points: expected_value) + + assert_equal expected_value, p.array_of_points + p.save! + p.reload + assert_equal expected_value, p.array_of_points + end + + def test_legacy_column + column = PostgresqlPoint.columns_hash["legacy_x"] + assert_equal :point, column.type + assert_equal "point", column.sql_type + assert_not column.array? + + type = PostgresqlPoint.type_for_attribute("legacy_x") + assert_not type.binary? + end + + def test_legacy_default + assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['legacy_y'] + assert_equal [12.2, 13.3], PostgresqlPoint.new.legacy_y + + assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['legacy_z'] + assert_equal [14.4, 15.5], PostgresqlPoint.new.legacy_z + end + + def test_legacy_schema_dumping + output = dump_table_schema("postgresql_points") + assert_match %r{t\.point\s+"legacy_x"$}, output + assert_match %r{t\.point\s+"legacy_y",\s+default: \[12\.2, 13\.3\]$}, output + assert_match %r{t\.point\s+"legacy_z",\s+default: \[14\.4, 15\.5\]$}, output + end + + def test_legacy_roundtrip + PostgresqlPoint.create! legacy_x: [10, 25.2] + record = PostgresqlPoint.first + assert_equal [10, 25.2], record.legacy_x + + record.legacy_x = [1.1, 2.2] + record.save! + assert record.reload + assert_equal [1.1, 2.2], record.legacy_x + end + + def test_legacy_mutation + p = PostgresqlPoint.create! legacy_x: [10, 20] + + p.legacy_x[1] = 25 + p.save! + p.reload + + assert_equal [10.0, 25.0], p.legacy_x assert_not p.changed? end end -class PostgresqlGeometricTest < ActiveRecord::TestCase +class PostgresqlGeometricTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + class PostgresqlGeometric < ActiveRecord::Base; end setup do @connection = ActiveRecord::Base.connection @connection.create_table("postgresql_geometrics") do |t| - t.column :a_line_segment, :lseg - t.column :a_box, :box - t.column :a_path, :path - t.column :a_polygon, :polygon - t.column :a_circle, :circle + t.lseg :a_line_segment + t.box :a_box + t.path :a_path + t.polygon :a_polygon + t.circle :a_circle end end @@ -136,4 +242,144 @@ class PostgresqlGeometricTest < ActiveRecord::TestCase objs = PostgresqlGeometric.find_by_sql "SELECT isclosed(a_path) FROM postgresql_geometrics ORDER BY id ASC" assert_equal [false, true], objs.map(&:isclosed) end + + def test_schema_dumping + output = dump_table_schema("postgresql_geometrics") + assert_match %r{t\.lseg\s+"a_line_segment"$}, output + assert_match %r{t\.box\s+"a_box"$}, output + assert_match %r{t\.path\s+"a_path"$}, output + assert_match %r{t\.polygon\s+"a_polygon"$}, output + assert_match %r{t\.circle\s+"a_circle"$}, output + end +end + +class PostgreSQLGeometricLineTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + class PostgresqlLine < ActiveRecord::Base; end + + setup do + unless ActiveRecord::Base.connection.send(:postgresql_version) >= 90400 + skip("line type is not fully implemented") + end + @connection = ActiveRecord::Base.connection + @connection.create_table("postgresql_lines") do |t| + t.line :a_line + end + end + + teardown do + if defined?(@connection) + @connection.drop_table 'postgresql_lines', if_exists: true + end + end + + def test_geometric_line_type + g = PostgresqlLine.new( + a_line: '{2.0, 3, 5.5}' + ) + g.save! + + h = PostgresqlLine.find(g.id) + assert_equal '{2,3,5.5}', h.a_line + end + + def test_alternative_format_line_type + g = PostgresqlLine.new( + a_line: '(2.0, 3), (4.0, 6.0)' + ) + g.save! + + h = PostgresqlLine.find(g.id) + assert_equal '{1.5,-1,0}', h.a_line + end + + def test_schema_dumping_for_line_type + output = dump_table_schema("postgresql_lines") + assert_match %r{t\.line\s+"a_line"$}, output + end +end + +class PostgreSQLGeometricTypesTest < ActiveRecord::PostgreSQLTestCase + attr_reader :connection, :table_name + + def setup + super + @connection = ActiveRecord::Base.connection + @table_name = :testings + end + + def test_creating_column_with_point_type + connection.create_table(table_name) do |t| + t.point :foo_point + end + + assert_column_exists(:foo_point) + assert_type_correct(:foo_point, :point) + end + + def test_creating_column_with_line_type + connection.create_table(table_name) do |t| + t.line :foo_line + end + + assert_column_exists(:foo_line) + assert_type_correct(:foo_line, :line) + end + + def test_creating_column_with_lseg_type + connection.create_table(table_name) do |t| + t.lseg :foo_lseg + end + + assert_column_exists(:foo_lseg) + assert_type_correct(:foo_lseg, :lseg) + end + + def test_creating_column_with_box_type + connection.create_table(table_name) do |t| + t.box :foo_box + end + + assert_column_exists(:foo_box) + assert_type_correct(:foo_box, :box) + end + + def test_creating_column_with_path_type + connection.create_table(table_name) do |t| + t.path :foo_path + end + + assert_column_exists(:foo_path) + assert_type_correct(:foo_path, :path) + end + + def test_creating_column_with_polygon_type + connection.create_table(table_name) do |t| + t.polygon :foo_polygon + end + + assert_column_exists(:foo_polygon) + assert_type_correct(:foo_polygon, :polygon) + end + + def test_creating_column_with_circle_type + connection.create_table(table_name) do |t| + t.circle :foo_circle + end + + assert_column_exists(:foo_circle) + assert_type_correct(:foo_circle, :circle) + end + + private + + def assert_column_exists(column_name) + assert connection.column_exists?(table_name, column_name) + end + + def assert_type_correct(column_name, type) + column = connection.columns(table_name).find { |c| c.name == column_name.to_s } + assert_equal type, column.type + end end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index ad9dd311a6..27cc65a643 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -2,7 +2,7 @@ require "cases/helper" require 'support/schema_dumping_helper' if ActiveRecord::Base.connection.supports_extensions? - class PostgresqlHstoreTest < ActiveRecord::TestCase + class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class Hstore < ActiveRecord::Base self.table_name = 'hstores' @@ -86,7 +86,7 @@ if ActiveRecord::Base.connection.supports_extensions? end def test_hstore_migration - hstore_migration = Class.new(ActiveRecord::Migration) do + hstore_migration = Class.new(ActiveRecord::Migration::Current) do def change change_table("hstores") do |t| t.hstore :keys diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb index 24199c69b8..bfda933fa4 100644 --- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class PostgresqlInfinityTest < ActiveRecord::TestCase +class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase include InTimeZone class PostgresqlInfinity < ActiveRecord::Base @@ -24,6 +24,15 @@ class PostgresqlInfinityTest < ActiveRecord::TestCase assert_equal Float::INFINITY, record.float end + test "type casting string on a float column" do + record = PostgresqlInfinity.new(float: 'Infinity') + assert_equal Float::INFINITY, record.float + record = PostgresqlInfinity.new(float: '-Infinity') + assert_equal(-Float::INFINITY, record.float) + record = PostgresqlInfinity.new(float: 'NaN') + assert_send [record.float, :nan?] + end + test "update_all with infinity on a float column" do record = PostgresqlInfinity.create! PostgresqlInfinity.update_all(float: Float::INFINITY) diff --git a/activerecord/test/cases/adapters/postgresql/integer_test.rb b/activerecord/test/cases/adapters/postgresql/integer_test.rb index 679a0fc7b3..b4e55964b9 100644 --- a/activerecord/test/cases/adapters/postgresql/integer_test.rb +++ b/activerecord/test/cases/adapters/postgresql/integer_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require "active_support/core_ext/numeric/bytes" -class PostgresqlIntegerTest < ActiveRecord::TestCase +class PostgresqlIntegerTest < ActiveRecord::PostgreSQLTestCase class PgInteger < ActiveRecord::Base end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 6878516aeb..663de680b5 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- require "cases/helper" require 'support/schema_dumping_helper' @@ -39,7 +38,7 @@ module PostgresqlJSONSharedTestCases end def test_default - @connection.add_column 'json_data_type', 'permissions', column_type, default: '{"users": "read", "posts": ["read", "write"]}' + @connection.add_column 'json_data_type', 'permissions', column_type, default: {"users": "read", "posts": ["read", "write"]} JsonDataType.reset_column_information assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.column_defaults['permissions']) @@ -179,16 +178,23 @@ module PostgresqlJSONSharedTestCases assert_not json.changed? end - def test_assigning_invalid_json - json = JsonDataType.new + def test_assigning_string_literal + json = JsonDataType.create(payload: "foo") + assert_equal "foo", json.payload + end - json.payload = 'foo' + def test_assigning_number + json = JsonDataType.create(payload: 1.234) + assert_equal 1.234, json.payload + end - assert_nil json.payload + def test_assigning_boolean + json = JsonDataType.create(payload: true) + assert_equal true, json.payload end end -class PostgresqlJSONTest < ActiveRecord::TestCase +class PostgresqlJSONTest < ActiveRecord::PostgreSQLTestCase include PostgresqlJSONSharedTestCases def column_type @@ -196,7 +202,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase end end -class PostgresqlJSONBTest < ActiveRecord::TestCase +class PostgresqlJSONBTest < ActiveRecord::PostgreSQLTestCase include PostgresqlJSONSharedTestCases def column_type diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb index ce0ad16557..56516c82b4 100644 --- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlLtreeTest < ActiveRecord::TestCase +class PostgresqlLtreeTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class Ltree < ActiveRecord::Base self.table_name = 'ltrees' diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index cedd399380..c031178479 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlMoneyTest < ActiveRecord::TestCase +class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class PostgresqlMoney < ActiveRecord::Base; end @@ -56,7 +56,7 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase def test_schema_dumping output = dump_table_schema("postgresql_moneys") assert_match %r{t\.money\s+"wealth",\s+scale: 2$}, output - assert_match %r{t\.money\s+"depth",\s+scale: 2,\s+default: 150\.55$}, output + assert_match %r{t\.money\s+"depth",\s+scale: 2,\s+default: "150\.55"$}, output end def test_create_and_update_money diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb index 033695518e..fe6ee4e2d9 100644 --- a/activerecord/test/cases/adapters/postgresql/network_test.rb +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlNetworkTest < ActiveRecord::TestCase +class PostgresqlNetworkTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class PostgresqlNetworkAddress < ActiveRecord::Base; end diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb index 093b81fe8d..ba7e7dc9a3 100644 --- a/activerecord/test/cases/adapters/postgresql/numbers_test.rb +++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class PostgresqlNumberTest < ActiveRecord::TestCase +class PostgresqlNumberTest < ActiveRecord::PostgreSQLTestCase class PostgresqlNumber < ActiveRecord::Base; end setup do @@ -31,7 +31,7 @@ class PostgresqlNumberTest < ActiveRecord::TestCase assert_equal 123456.789, first.double assert_equal(-::Float::INFINITY, second.single) assert_equal ::Float::INFINITY, second.double - assert_same ::Float::NAN, third.double + assert_send [third.double, :nan?] end def test_update diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index a934180a43..9832df7839 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -4,7 +4,8 @@ require 'support/connection_helper' module ActiveRecord module ConnectionAdapters - class PostgreSQLAdapterTest < ActiveRecord::TestCase + class PostgreSQLAdapterTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false include DdlHelper include ConnectionHelper @@ -53,80 +54,44 @@ module ActiveRecord end end - def test_composite_primary_key - with_example_table 'id serial, number serial, PRIMARY KEY (id, number)' do - assert_nil @connection.primary_key('ex') - end - end - def test_primary_key_raises_error_if_table_not_found assert_raises(ActiveRecord::StatementInvalid) do @connection.primary_key('unobtainium') end end - def test_insert_sql_with_proprietary_returning_clause - with_example_table do - id = @connection.insert_sql("insert into ex (number) values(5150)", nil, "number") - assert_equal "5150", id - end - end - - def test_insert_sql_with_quoted_schema_and_table_name - with_example_table do - id = @connection.insert_sql('insert into "public"."ex" (number) values(5150)') - expect = @connection.query('select max(id) from ex').first.first - assert_equal expect, id - end - end - - def test_insert_sql_with_no_space_after_table_name - with_example_table do - id = @connection.insert_sql("insert into ex(number) values(5150)") - expect = @connection.query('select max(id) from ex').first.first - assert_equal expect, id - end - end - - def test_multiline_insert_sql - with_example_table do - id = @connection.insert_sql(<<-SQL) - insert into ex( - number) - values( - 5152 - ) - SQL - expect = @connection.query('select max(id) from ex').first.first - assert_equal expect, id - end + def test_exec_insert_with_returning_disabled + connection = connection_without_insert_returning + result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id', 'postgresql_partitioned_table_parent_id_seq') + expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first + assert_equal expect.to_i, result.rows.first.first end - def test_insert_sql_with_returning_disabled + def test_exec_insert_with_returning_disabled_and_no_sequence_name_given connection = connection_without_insert_returning - id = connection.insert_sql("insert into postgresql_partitioned_table_parent (number) VALUES (1)") + result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id') expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first - assert_equal expect, id + assert_equal expect.to_i, result.rows.first.first end - def test_exec_insert_with_returning_disabled + def test_exec_insert_default_values_with_returning_disabled_and_no_sequence_name_given connection = connection_without_insert_returning - result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id', 'postgresql_partitioned_table_parent_id_seq') + result = connection.exec_insert("insert into postgresql_partitioned_table_parent DEFAULT VALUES", nil, [], 'id') expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first - assert_equal expect, result.rows.first.first + assert_equal expect.to_i, result.rows.first.first end - def test_exec_insert_with_returning_disabled_and_no_sequence_name_given + def test_exec_insert_default_values_quoted_schema_with_returning_disabled_and_no_sequence_name_given connection = connection_without_insert_returning - result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], 'id') + result = connection.exec_insert('insert into "public"."postgresql_partitioned_table_parent" DEFAULT VALUES', nil, [], 'id') expect = connection.query('select max(id) from postgresql_partitioned_table_parent').first.first - assert_equal expect, result.rows.first.first + assert_equal expect.to_i, result.rows.first.first end def test_sql_for_insert_with_returning_disabled connection = connection_without_insert_returning - result = connection.sql_for_insert('sql', nil, nil, nil, 'binds') - assert_equal ['sql', 'binds'], result + sql, binds = connection.sql_for_insert('sql', nil, nil, nil, 'binds') + assert_equal ['sql', 'binds'], [sql, binds] end def test_serial_sequence @@ -231,30 +196,6 @@ module ActiveRecord @connection.drop_table 'ex2', if_exists: true end - def test_exec_insert_number - with_example_table do - insert(@connection, 'number' => 10) - - result = @connection.exec_query('SELECT number FROM ex WHERE number = 10') - - assert_equal 1, result.rows.length - assert_equal "10", result.rows.last.last - end - end - - def test_exec_insert_string - with_example_table do - str = 'いただきます!' - insert(@connection, 'number' => 10, 'data' => str) - - result = @connection.exec_query('SELECT number, data FROM ex WHERE number = 10') - - value = result.rows.last.last - - assert_equal str, value - end - end - def test_table_alias_length assert_nothing_raised do @connection.table_alias_length @@ -274,45 +215,42 @@ module ActiveRecord assert_equal 1, result.rows.length assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows end end - def test_exec_with_binds - with_example_table do - string = @connection.quote('foo') - @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - result = @connection.exec_query( - 'SELECT id, data FROM ex WHERE id = $1', nil, [bind_param(1)]) + if ActiveRecord::Base.connection.prepared_statements + def test_exec_with_binds + with_example_table do + string = @connection.quote('foo') + @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length + bind = Relation::QueryAttribute.new("id", 1, Type::Value.new) + result = @connection.exec_query('SELECT id, data FROM ex WHERE id = $1', nil, [bind]) + + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows + end end - end - def test_exec_typecasts_bind_vals - with_example_table do - string = @connection.quote('foo') - @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") + def test_exec_typecasts_bind_vals + with_example_table do + string = @connection.quote('foo') + @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - bind = ActiveRecord::Relation::QueryAttribute.new("id", "1-fuu", ActiveRecord::Type::Integer.new) - result = @connection.exec_query( - 'SELECT id, data FROM ex WHERE id = $1', nil, [bind]) + bind = Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new) + result = @connection.exec_query('SELECT id, data FROM ex WHERE id = $1', nil, [bind]) - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length + assert_equal 1, result.rows.length + assert_equal 2, result.columns.length - assert_equal [['1', 'foo']], result.rows + assert_equal [[1, 'foo']], result.rows + end end end - def test_substitute_at - bind = @connection.substitute_at(nil) - assert_equal Arel.sql('$1'), bind.to_sql - end - def test_partial_index with_example_table do @connection.add_index 'ex', %w{ id number }, :name => 'partial', :where => "number > 100" @@ -321,6 +259,22 @@ module ActiveRecord end end + def test_expression_index + with_example_table do + @connection.add_index 'ex', 'mod(id, 10), abs(number)', name: 'expression' + index = @connection.indexes('ex').find { |idx| idx.name == 'expression' } + assert_equal 'mod(id, 10), abs(number)', index.columns + end + end + + def test_index_with_opclass + with_example_table do + @connection.add_index 'ex', 'data varchar_pattern_ops', name: 'with_opclass' + index = @connection.indexes('ex').find { |idx| idx.name == 'with_opclass' } + assert_equal 'data varchar_pattern_ops', index.columns + end + end + def test_columns_for_distinct_zero_orders assert_equal "posts.id", @connection.columns_for_distinct("posts.id", []) @@ -435,19 +389,6 @@ module ActiveRecord end private - def insert(ctx, data) - binds = data.map { |name, value| - bind_param(value, name) - } - columns = binds.map(&:name) - - bind_subs = columns.length.times.map { |x| "$#{x + 1}" } - - sql = "INSERT INTO ex (#{columns.join(", ")}) - VALUES (#{bind_subs.join(', ')})" - - ctx.exec_insert(sql, 'SQL', binds) - end def with_example_table(definition = 'id serial primary key, number integer, data character varying(255)', &block) super(@connection, 'ex', definition, &block) @@ -456,10 +397,6 @@ module ActiveRecord def connection_without_insert_returning ActiveRecord::Base.postgresql_connection(ActiveRecord::Base.configurations['arunit'].merge(:insert_returning => false)) end - - def bind_param(value, name = nil) - ActiveRecord::Relation::QueryAttribute.new(name, value, ActiveRecord::Type::Value.new) - end end end end diff --git a/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb b/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb new file mode 100644 index 0000000000..f1519db48b --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb @@ -0,0 +1,22 @@ +require "cases/helper" +require "models/developer" + +class PreparedStatementsTest < ActiveRecord::PostgreSQLTestCase + fixtures :developers + + def setup + @default_prepared_statements = Developer.connection_config[:prepared_statements] + Developer.connection_config[:prepared_statements] = false + end + + def teardown + Developer.connection_config[:prepared_statements] = @default_prepared_statements + end + + def nothing_raised_with_falsy_prepared_statements + assert_nothing_raised do + Developer.where(id: 1) + end + end + +end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index e4420d9d13..5e6f4dbbb8 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -4,7 +4,7 @@ require 'ipaddr' module ActiveRecord module ConnectionAdapters class PostgreSQLAdapter - class QuotingTest < ActiveRecord::TestCase + class QuotingTest < ActiveRecord::PostgreSQLTestCase def setup @conn = ActiveRecord::Base.connection end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index bbf96278b0..0edfa4ed9d 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -1,14 +1,16 @@ require "cases/helper" require 'support/connection_helper' -if ActiveRecord::Base.connection.supports_ranges? +if ActiveRecord::Base.connection.respond_to?(:supports_ranges?) && ActiveRecord::Base.connection.supports_ranges? class PostgresqlRange < ActiveRecord::Base self.table_name = "postgresql_ranges" + self.time_zone_aware_types += [:tsrange, :tstzrange] end - class PostgresqlRangeTest < ActiveRecord::TestCase + class PostgresqlRangeTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false include ConnectionHelper + include InTimeZone def setup @connection = PostgresqlRange.connection @@ -160,6 +162,26 @@ _SQL assert_nil @empty_range.float_range end + def test_timezone_awareness_tzrange + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) + + record = PostgresqlRange.new(tstz_range: time_string..time_string) + assert_equal time..time, record.tstz_range + assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone + + record.save! + record.reload + + assert_equal time..time, record.tstz_range + assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone + end + end + def test_create_tstzrange tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT') round_trip(@new_range, :tstz_range, tstzrange) @@ -188,6 +210,26 @@ _SQL Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0)) end + def test_timezone_awareness_tsrange + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) + + record = PostgresqlRange.new(ts_range: time_string..time_string) + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + + record.save! + record.reload + + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + end + end + def test_create_numrange assert_equal_round_trip(@new_range, :num_range, BigDecimal.new('0.5')...BigDecimal.new('1')) diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb index d76e762815..c895ab9db5 100644 --- a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' require 'support/connection_helper' -class PostgreSQLReferentialIntegrityTest < ActiveRecord::TestCase +class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false include ConnectionHelper @@ -106,6 +106,6 @@ class PostgreSQLReferentialIntegrityTest < ActiveRecord::TestCase private def assert_transaction_is_not_broken - assert_equal "1", @connection.select_value("SELECT 1") + assert_equal 1, @connection.select_value("SELECT 1") end end diff --git a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb index f507328868..bd64bae308 100644 --- a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb +++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class PostgresqlRenameTableTest < ActiveRecord::TestCase +class PostgresqlRenameTableTest < ActiveRecord::PostgreSQLTestCase def setup @connection = ActiveRecord::Base.connection @connection.create_table :before_rename, force: true diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index 359a45bbd1..285a92f60e 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -3,7 +3,7 @@ require "cases/helper" class SchemaThing < ActiveRecord::Base end -class SchemaAuthorizationTest < ActiveRecord::TestCase +class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false TABLE_NAME = 'schema_things' @@ -31,7 +31,7 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase set_session_auth @connection.execute "RESET search_path" USERS.each do |u| - @connection.execute "DROP SCHEMA #{u} CASCADE" + @connection.drop_schema u @connection.execute "DROP USER #{u}" end end @@ -55,20 +55,22 @@ class SchemaAuthorizationTest < ActiveRecord::TestCase set_session_auth USERS.each do |u| set_session_auth u - assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [bind_param(1)]).first['name'] + assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = 1") set_session_auth end end end - def test_auth_with_bind - assert_nothing_raised do - set_session_auth - USERS.each do |u| - @connection.clear_cache! - set_session_auth u - assert_equal u, @connection.exec_query("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [bind_param(1)]).first['name'] + if ActiveRecord::Base.connection.prepared_statements + def test_auth_with_bind + assert_nothing_raised do set_session_auth + USERS.each do |u| + @connection.clear_cache! + set_session_auth u + assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = $1", 'SQL', [bind_param(1)]) + set_session_auth + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index c1be340e7c..52ef07f654 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -2,7 +2,19 @@ require "cases/helper" require 'models/default' require 'support/schema_dumping_helper' -class SchemaTest < ActiveRecord::TestCase +module PGSchemaHelper + def with_schema_search_path(schema_search_path) + @connection.schema_search_path = schema_search_path + @connection.schema_cache.clear! + yield if block_given? + ensure + @connection.schema_search_path = "'$user', public" + @connection.schema_cache.clear! + end +end + +class SchemaTest < ActiveRecord::PostgreSQLTestCase + include PGSchemaHelper self.use_transactional_tests = false SCHEMA_NAME = 'test_schema' @@ -84,8 +96,8 @@ class SchemaTest < ActiveRecord::TestCase end teardown do - @connection.execute "DROP SCHEMA #{SCHEMA2_NAME} CASCADE" - @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE" + @connection.drop_schema SCHEMA2_NAME, if_exists: true + @connection.drop_schema SCHEMA_NAME, if_exists: true end def test_schema_names @@ -121,10 +133,17 @@ class SchemaTest < ActiveRecord::TestCase assert !@connection.schema_names.include?("test_schema3") end + def test_drop_schema_if_exists + @connection.create_schema "some_schema" + assert_includes @connection.schema_names, "some_schema" + @connection.drop_schema "some_schema", if_exists: true + assert_not_includes @connection.schema_names, "some_schema" + end + def test_habtm_table_name_with_schema + ActiveRecord::Base.connection.drop_schema "music", if_exists: true + ActiveRecord::Base.connection.create_schema "music" ActiveRecord::Base.connection.execute <<-SQL - DROP SCHEMA IF EXISTS music CASCADE; - CREATE SCHEMA music; CREATE TABLE music.albums (id serial primary key); CREATE TABLE music.songs (id serial primary key); CREATE TABLE music.albums_songs (album_id integer, song_id integer); @@ -134,69 +153,75 @@ class SchemaTest < ActiveRecord::TestCase Album.create assert_equal song, Song.includes(:albums).references(:albums).first ensure - ActiveRecord::Base.connection.execute "DROP SCHEMA music CASCADE;" + ActiveRecord::Base.connection.drop_schema "music", if_exists: true end - def test_raise_drop_schema_with_nonexisting_schema + def test_drop_schema_with_nonexisting_schema assert_raises(ActiveRecord::StatementInvalid) do - @connection.drop_schema "test_schema3" + @connection.drop_schema "idontexist" + end + + assert_nothing_raised do + @connection.drop_schema "idontexist", if_exists: true end end - def test_raise_wraped_exception_on_bad_prepare + def test_raise_wrapped_exception_on_bad_prepare assert_raises(ActiveRecord::StatementInvalid) do - @connection.exec_query "select * from developers where id = ?", 'sql', [[nil, 1]] + @connection.exec_query "select * from developers where id = ?", 'sql', [bind_param(1)] end end - def test_schema_change_with_prepared_stmt - altered = false - @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)] - @connection.exec_query "alter table developers add column zomg int", 'sql', [] - altered = true - @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)] - ensure - # We are not using DROP COLUMN IF EXISTS because that syntax is only - # supported by pg 9.X - @connection.exec_query("alter table developers drop column zomg", 'sql', []) if altered + if ActiveRecord::Base.connection.prepared_statements + def test_schema_change_with_prepared_stmt + altered = false + @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)] + @connection.exec_query "alter table developers add column zomg int", 'sql', [] + altered = true + @connection.exec_query "select * from developers where id = $1", 'sql', [bind_param(1)] + ensure + # We are not using DROP COLUMN IF EXISTS because that syntax is only + # supported by pg 9.X + @connection.exec_query("alter table developers drop column zomg", 'sql', []) if altered + end end - def test_table_exists? + def test_data_source_exists? [Thing1, Thing2, Thing3, Thing4].each do |klass| name = klass.table_name - assert @connection.table_exists?(name), "'#{name}' table should exist" + assert @connection.data_source_exists?(name), "'#{name}' data_source should exist" end end - def test_table_exists_when_on_schema_search_path + def test_data_source_exists_when_on_schema_search_path with_schema_search_path(SCHEMA_NAME) do - assert(@connection.table_exists?(TABLE_NAME), "table should exist and be found") + assert(@connection.data_source_exists?(TABLE_NAME), "data_source should exist and be found") end end - def test_table_exists_when_not_on_schema_search_path + def test_data_source_exists_when_not_on_schema_search_path with_schema_search_path('PUBLIC') do - assert(!@connection.table_exists?(TABLE_NAME), "table exists but should not be found") + assert(!@connection.data_source_exists?(TABLE_NAME), "data_source exists but should not be found") end end - def test_table_exists_wrong_schema - assert(!@connection.table_exists?("foo.things"), "table should not exist") + def test_data_source_exists_wrong_schema + assert(!@connection.data_source_exists?("foo.things"), "data_source should not exist") end - def test_table_exists_quoted_names + def test_data_source_exists_quoted_names [ %("#{SCHEMA_NAME}"."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}")].each do |given| - assert(@connection.table_exists?(given), "table should exist when specified as #{given}") + assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}") end with_schema_search_path(SCHEMA_NAME) do given = %("#{TABLE_NAME}") - assert(@connection.table_exists?(given), "table should exist when specified as #{given}") + assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}") end end - def test_table_exists_quoted_table + def test_data_source_exists_quoted_table with_schema_search_path(SCHEMA_NAME) do - assert(@connection.table_exists?('"things.table"'), "table should exist") + assert(@connection.data_source_exists?('"things.table"'), "data_source should exist") end end @@ -298,16 +323,33 @@ class SchemaTest < ActiveRecord::TestCase do_dump_index_tests_for_schema("public, #{SCHEMA_NAME}", INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN, INDEX_E_COLUMN) end + def test_dump_indexes_for_table_with_scheme_specified_in_name + indexes = @connection.indexes("#{SCHEMA_NAME}.#{TABLE_NAME}") + assert_equal 5, indexes.size + end + def test_with_uppercase_index_name @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" - assert_nothing_raised { @connection.remove_index! "things", "#{SCHEMA_NAME}.things_Index"} - @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" with_schema_search_path SCHEMA_NAME do - assert_nothing_raised { @connection.remove_index! "things", "things_Index"} + assert_nothing_raised { @connection.remove_index "things", name: "things_Index"} end end + def test_remove_index_when_schema_specified + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_nothing_raised { @connection.remove_index "things", name: "#{SCHEMA_NAME}.things_Index" } + + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_nothing_raised { @connection.remove_index "#{SCHEMA_NAME}.things", name: "things_Index" } + + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_nothing_raised { @connection.remove_index "#{SCHEMA_NAME}.things", name: "#{SCHEMA_NAME}.things_Index" } + + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_raises(ArgumentError) { @connection.remove_index "#{SCHEMA2_NAME}.things", name: "#{SCHEMA_NAME}.things_Index" } + end + def test_primary_key_with_schema_specified [ %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"), @@ -384,16 +426,16 @@ class SchemaTest < ActiveRecord::TestCase def test_reset_pk_sequence sequence_name = "#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}" @connection.execute "SELECT setval('#{sequence_name}', 123)" - assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')") + assert_equal 124, @connection.select_value("SELECT nextval('#{sequence_name}')") @connection.reset_pk_sequence!("#{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME}") - assert_equal "1", @connection.select_value("SELECT nextval('#{sequence_name}')") + assert_equal 1, @connection.select_value("SELECT nextval('#{sequence_name}')") end def test_set_pk_sequence table_name = "#{SCHEMA_NAME}.#{PK_TABLE_NAME}" _, sequence_name = @connection.pk_and_sequence_for table_name @connection.set_pk_sequence! table_name, 123 - assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')") + assert_equal 124, @connection.select_value("SELECT nextval('#{sequence_name}')") @connection.reset_pk_sequence! table_name end @@ -404,28 +446,25 @@ class SchemaTest < ActiveRecord::TestCase end end - def with_schema_search_path(schema_search_path) - @connection.schema_search_path = schema_search_path - yield if block_given? - ensure - @connection.schema_search_path = "'$user', public" - end - def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name, fourth_index_column_name) with_schema_search_path(this_schema_name) do indexes = @connection.indexes(TABLE_NAME).sort_by(&:name) - assert_equal 4,indexes.size - - do_dump_index_assertions_for_one_index(indexes[0], INDEX_A_NAME, first_index_column_name) - do_dump_index_assertions_for_one_index(indexes[1], INDEX_B_NAME, second_index_column_name) - do_dump_index_assertions_for_one_index(indexes[2], INDEX_D_NAME, third_index_column_name) - do_dump_index_assertions_for_one_index(indexes[3], INDEX_E_NAME, fourth_index_column_name) - - indexes.select{|i| i.name != INDEX_E_NAME}.each do |index| - assert_equal :btree, index.using - end - assert_equal :gin, indexes.select{|i| i.name == INDEX_E_NAME}[0].using - assert_equal :desc, indexes.select{|i| i.name == INDEX_D_NAME}[0].orders[INDEX_D_COLUMN] + assert_equal 5, indexes.size + + index_a, index_b, index_c, index_d, index_e = indexes + + do_dump_index_assertions_for_one_index(index_a, INDEX_A_NAME, first_index_column_name) + do_dump_index_assertions_for_one_index(index_b, INDEX_B_NAME, second_index_column_name) + do_dump_index_assertions_for_one_index(index_d, INDEX_D_NAME, third_index_column_name) + do_dump_index_assertions_for_one_index(index_e, INDEX_E_NAME, fourth_index_column_name) + + assert_equal :btree, index_a.using + assert_equal :btree, index_b.using + assert_equal :gin, index_c.using + assert_equal :btree, index_d.using + assert_equal :gin, index_e.using + + assert_equal :desc, index_d.orders[INDEX_D_COLUMN] end end @@ -441,7 +480,7 @@ class SchemaTest < ActiveRecord::TestCase end end -class SchemaForeignKeyTest < ActiveRecord::TestCase +class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper setup do @@ -462,14 +501,14 @@ class SchemaForeignKeyTest < ActiveRecord::TestCase ensure @connection.drop_table "wagons", if_exists: true @connection.drop_table "my_schema.trains", if_exists: true - @connection.execute "DROP SCHEMA IF EXISTS my_schema" + @connection.drop_schema "my_schema", if_exists: true end end -class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase +class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCase setup do @connection = ActiveRecord::Base.connection - @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE" + @connection.drop_schema "schema_1", if_exists: true @connection.execute "CREATE SCHEMA schema_1" @connection.execute "CREATE DOMAIN schema_1.text AS text" @connection.execute "CREATE DOMAIN schema_1.varchar AS varchar" @@ -480,13 +519,14 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase @connection.create_table "defaults" do |t| t.text "text_col", default: "some value" t.string "string_col", default: "some value" + t.decimal "decimal_col", default: "3.14159265358979323846" end Default.reset_column_information end teardown do @connection.schema_search_path = @old_search_path - @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE" + @connection.drop_schema "schema_1", if_exists: true Default.reset_column_information end @@ -498,6 +538,10 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase assert_equal "some value", Default.new.string_col, "Default of string column was not correctly parsed" end + def test_decimal_defaults_in_new_schema_when_overriding_domain + assert_equal BigDecimal.new("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed" + end + def test_bpchar_defaults_in_new_schema_when_overriding_domain @connection.execute "ALTER TABLE defaults ADD bpchar_col bpchar DEFAULT 'some value'" Default.reset_column_information @@ -514,3 +558,40 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase assert_equal "foo'::bar", Default.new.string_col end end + +class SchemaWithDotsTest < ActiveRecord::PostgreSQLTestCase + include PGSchemaHelper + self.use_transactional_tests = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_schema "my.schema" + end + + teardown do + @connection.drop_schema "my.schema", if_exists: true + end + + test "rename_table" do + with_schema_search_path('"my.schema"') do + @connection.create_table :posts + @connection.rename_table :posts, :articles + assert_equal ["articles"], @connection.tables + end + end + + test "Active Record basics" do + with_schema_search_path('"my.schema"') do + @connection.create_table :articles do |t| + t.string :title + end + article_class = Class.new(ActiveRecord::Base) do + self.table_name = '"my.schema".articles' + end + + article_class.create!(title: "zOMG, welcome to my blorgh!") + welcome_article = article_class.last + assert_equal "zOMG, welcome to my blorgh!", welcome_article.title + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/serial_test.rb b/activerecord/test/cases/adapters/postgresql/serial_test.rb index 458a8dae6c..8abe064bf1 100644 --- a/activerecord/test/cases/adapters/postgresql/serial_test.rb +++ b/activerecord/test/cases/adapters/postgresql/serial_test.rb @@ -1,7 +1,7 @@ require "cases/helper" require 'support/schema_dumping_helper' -class PostgresqlSerialTest < ActiveRecord::TestCase +class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class PostgresqlSerial < ActiveRecord::Base; end @@ -10,6 +10,7 @@ class PostgresqlSerialTest < ActiveRecord::TestCase @connection = ActiveRecord::Base.connection @connection.create_table "postgresql_serials", force: true do |t| t.serial :seq + t.integer :serials_id, default: -> { "nextval('postgresql_serials_id_seq')" } end end @@ -24,13 +25,25 @@ class PostgresqlSerialTest < ActiveRecord::TestCase assert column.serial? end + def test_not_serial_column + column = PostgresqlSerial.columns_hash["serials_id"] + assert_equal :integer, column.type + assert_equal "integer", column.sql_type + assert_not column.serial? + end + def test_schema_dump_with_shorthand output = dump_table_schema "postgresql_serials" - assert_match %r{t\.serial\s+"seq"}, output + assert_match %r{t\.serial\s+"seq",\s+null: false$}, output + end + + def test_schema_dump_with_not_serial + output = dump_table_schema "postgresql_serials" + assert_match %r{t\.integer\s+"serials_id",\s+default: -> \{ "nextval\('postgresql_serials_id_seq'::regclass\)" \}$}, output end end -class PostgresqlBigSerialTest < ActiveRecord::TestCase +class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class PostgresqlBigSerial < ActiveRecord::Base; end @@ -39,6 +52,7 @@ class PostgresqlBigSerialTest < ActiveRecord::TestCase @connection = ActiveRecord::Base.connection @connection.create_table "postgresql_big_serials", force: true do |t| t.bigserial :seq + t.bigint :serials_id, default: -> { "nextval('postgresql_big_serials_id_seq')" } end end @@ -53,8 +67,20 @@ class PostgresqlBigSerialTest < ActiveRecord::TestCase assert column.serial? end + def test_not_bigserial_column + column = PostgresqlBigSerial.columns_hash["serials_id"] + assert_equal :integer, column.type + assert_equal "bigint", column.sql_type + assert_not column.serial? + end + def test_schema_dump_with_shorthand output = dump_table_schema "postgresql_big_serials" - assert_match %r{t\.bigserial\s+"seq"}, output + assert_match %r{t\.bigserial\s+"seq",\s+null: false$}, output + end + + def test_schema_dump_with_not_bigserial + output = dump_table_schema "postgresql_big_serials" + assert_match %r{t\.bigint\s+"serials_id",\s+default: -> \{ "nextval\('postgresql_big_serials_id_seq'::regclass\)" \}$}, output end end diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb index 1497b0abc7..5aab246c99 100644 --- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb @@ -13,7 +13,7 @@ module ActiveRecord end end - class StatementPoolTest < ActiveRecord::TestCase + class StatementPoolTest < ActiveRecord::PostgreSQLTestCase if Process.respond_to?(:fork) def test_cache_is_per_pid cache = StatementPool.new nil, 10 diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index a639f98272..4c4866b46b 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -2,7 +2,7 @@ require 'cases/helper' require 'models/developer' require 'models/topic' -class PostgresqlTimestampTest < ActiveRecord::TestCase +class PostgresqlTimestampTest < ActiveRecord::PostgreSQLTestCase class PostgresqlTimestampWithZone < ActiveRecord::Base; end self.use_transactional_tests = false @@ -43,7 +43,7 @@ class PostgresqlTimestampTest < ActiveRecord::TestCase end end -class TimestampTest < ActiveRecord::TestCase +class PostgresqlTimestampFixtureTest < ActiveRecord::PostgreSQLTestCase fixtures :topics def test_group_by_date diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb index c0907b8f21..ea0f0b8fa5 100644 --- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -1,6 +1,6 @@ require 'cases/helper' -class PostgresqlTypeLookupTest < ActiveRecord::TestCase +class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase setup do @connection = ActiveRecord::Base.connection end @@ -18,7 +18,7 @@ class PostgresqlTypeLookupTest < ActiveRecord::TestCase bigint_array = @connection.type_map.lookup(1016, -1, "bigint[]") big_array = [123456789123456789] - assert_raises(RangeError) { int_array.serialize(big_array) } + assert_raises(ActiveModel::RangeError) { int_array.serialize(big_array) } assert_equal "{123456789123456789}", bigint_array.serialize(big_array) end @@ -27,7 +27,7 @@ class PostgresqlTypeLookupTest < ActiveRecord::TestCase bigint_range = @connection.type_map.lookup(3926, -1, "int8range") big_range = 0..123456789123456789 - assert_raises(RangeError) { int_range.serialize(big_range) } + assert_raises(ActiveModel::RangeError) { int_range.serialize(big_range) } assert_equal "[0,123456789123456789]", bigint_range.serialize(big_range) end end diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb index 3fdb6888d9..095c1826e5 100644 --- a/activerecord/test/cases/adapters/postgresql/utils_test.rb +++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb @@ -1,6 +1,7 @@ require 'cases/helper' +require 'active_record/connection_adapters/postgresql/utils' -class PostgreSQLUtilsTest < ActiveSupport::TestCase +class PostgreSQLUtilsTest < ActiveRecord::PostgreSQLTestCase Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name include ActiveRecord::ConnectionAdapters::PostgreSQL::Utils @@ -20,7 +21,7 @@ class PostgreSQLUtilsTest < ActiveSupport::TestCase end end -class PostgreSQLNameTest < ActiveSupport::TestCase +class PostgreSQLNameTest < ActiveRecord::PostgreSQLTestCase Name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name test "represents itself as schema.name" do diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index e9379a1019..7628075ad2 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -11,7 +11,7 @@ module PostgresqlUUIDHelper end end -class PostgresqlUUIDTest < ActiveRecord::TestCase +class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase include PostgresqlUUIDHelper include SchemaDumpingHelper @@ -20,6 +20,8 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase end setup do + enable_extension!('uuid-ossp', connection) + connection.create_table "uuid_data_type" do |t| t.uuid 'guid' end @@ -135,7 +137,7 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase end end -class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase +class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase include PostgresqlUUIDHelper include SchemaDumpingHelper @@ -144,8 +146,6 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase end setup do - enable_extension!('uuid-ossp', connection) - connection.create_table('pg_uuids', id: :uuid, default: 'uuid_generate_v1()') do |t| t.string 'name' t.uuid 'other_uuid', default: 'uuid_generate_v4()' @@ -170,7 +170,6 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase drop_table "pg_uuids" drop_table 'pg_uuids_2' connection.execute 'DROP FUNCTION IF EXISTS my_uuid_generator();' - disable_extension!('uuid-ossp', connection) end if ActiveRecord::Base.connection.supports_extensions? @@ -198,25 +197,23 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase def test_schema_dumper_for_uuid_primary_key schema = dump_table_schema "pg_uuids" - assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema) - assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema) + assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: -> { "uuid_generate_v1\(\)" }/, schema) + assert_match(/t\.uuid "other_uuid", default: -> { "uuid_generate_v4\(\)" }/, schema) end def test_schema_dumper_for_uuid_primary_key_with_custom_default schema = dump_table_schema "pg_uuids_2" - assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: "my_uuid_generator\(\)"/, schema) - assert_match(/t\.uuid "other_uuid_2", default: "my_uuid_generator\(\)"/, schema) + assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: -> { "my_uuid_generator\(\)" }/, schema) + assert_match(/t\.uuid "other_uuid_2", default: -> { "my_uuid_generator\(\)" }/, schema) end end end -class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase +class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase include PostgresqlUUIDHelper include SchemaDumpingHelper setup do - enable_extension!('uuid-ossp', connection) - connection.create_table('pg_uuids', id: false) do |t| t.primary_key :id, :uuid, default: nil t.string 'name' @@ -225,7 +222,6 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase teardown do drop_table "pg_uuids" - disable_extension!('uuid-ossp', connection) end if ActiveRecord::Base.connection.supports_extensions? @@ -244,7 +240,7 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase end end -class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase +class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase include PostgresqlUUIDHelper class UuidPost < ActiveRecord::Base @@ -258,8 +254,6 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase end setup do - enable_extension!('uuid-ossp', connection) - connection.transaction do connection.create_table('pg_uuid_posts', id: :uuid) do |t| t.string 'title' @@ -274,7 +268,6 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase teardown do drop_table "pg_uuid_comments" drop_table "pg_uuid_posts" - disable_extension!('uuid-ossp', connection) end if ActiveRecord::Base.connection.supports_extensions? diff --git a/activerecord/test/cases/adapters/postgresql/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb deleted file mode 100644 index 8a8e1d3b17..0000000000 --- a/activerecord/test/cases/adapters/postgresql/view_test.rb +++ /dev/null @@ -1,63 +0,0 @@ -require "cases/helper" -require "cases/view_test" - -class UpdateableViewTest < ActiveRecord::TestCase - fixtures :books - - class PrintedBook < ActiveRecord::Base - self.primary_key = "id" - end - - setup do - @connection = ActiveRecord::Base.connection - @connection.execute <<-SQL - CREATE VIEW printed_books - AS SELECT id, name, status, format FROM books WHERE format = 'paperback' - SQL - end - - teardown do - @connection.execute "DROP VIEW printed_books" if @connection.table_exists? "printed_books" - end - - def test_update_record - book = PrintedBook.first - book.name = "AWDwR" - book.save! - book.reload - assert_equal "AWDwR", book.name - end - - def test_insert_record - PrintedBook.create! name: "Rails in Action", status: 0, format: "paperback" - - new_book = PrintedBook.last - assert_equal "Rails in Action", new_book.name - end - - def test_update_record_to_fail_view_conditions - book = PrintedBook.first - book.format = "ebook" - book.save! - - assert_raises ActiveRecord::RecordNotFound do - book.reload - end - end -end - -if ActiveRecord::Base.connection.supports_materialized_views? -class MaterializedViewTest < ActiveRecord::TestCase - include ViewBehavior - - private - def create_view(name, query) - @connection.execute "CREATE MATERIALIZED VIEW #{name} AS #{query}" - end - - def drop_view(name) - @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.table_exists? name - - end -end -end diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb index b097deb2f4..add32699fa 100644 --- a/activerecord/test/cases/adapters/postgresql/xml_test.rb +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' require 'support/schema_dumping_helper' -class PostgresqlXMLTest < ActiveRecord::TestCase +class PostgresqlXMLTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class XmlDataType < ActiveRecord::Base self.table_name = 'xml_data_type' diff --git a/activerecord/test/cases/adapters/sqlite3/collation_test.rb b/activerecord/test/cases/adapters/sqlite3/collation_test.rb new file mode 100644 index 0000000000..58a9469ce5 --- /dev/null +++ b/activerecord/test/cases/adapters/sqlite3/collation_test.rb @@ -0,0 +1,53 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class SQLite3CollationTest < ActiveRecord::SQLite3TestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :collation_table_sqlite3, force: true do |t| + t.string :string_nocase, collation: 'NOCASE' + t.text :text_rtrim, collation: 'RTRIM' + end + end + + def teardown + @connection.drop_table :collation_table_sqlite3, if_exists: true + end + + test "string column with collation" do + column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'string_nocase' } + assert_equal :string, column.type + assert_equal 'NOCASE', column.collation + end + + test "text column with collation" do + column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'text_rtrim' } + assert_equal :text, column.type + assert_equal 'RTRIM', column.collation + end + + test "add column with collation" do + @connection.add_column :collation_table_sqlite3, :title, :string, collation: 'RTRIM' + + column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'RTRIM', column.collation + end + + test "change column with collation" do + @connection.add_column :collation_table_sqlite3, :description, :string + @connection.change_column :collation_table_sqlite3, :description, :text, collation: 'RTRIM' + + column = @connection.columns(:collation_table_sqlite3).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'RTRIM', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("collation_table_sqlite3") + assert_match %r{t.string\s+"string_nocase",\s+collation: "NOCASE"$}, output + assert_match %r{t.text\s+"text_rtrim",\s+collation: "RTRIM"$}, output + end +end diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index 13b754d226..34e3b2e023 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -class CopyTableTest < ActiveRecord::TestCase +class CopyTableTest < ActiveRecord::SQLite3TestCase fixtures :customers def setup diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index 7d66c44798..a1a6e5f16a 100644 --- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb @@ -2,26 +2,20 @@ require "cases/helper" require 'models/developer' require 'models/computer' -module ActiveRecord - module ConnectionAdapters - class SQLite3Adapter - class ExplainTest < ActiveRecord::TestCase - fixtures :developers +class SQLite3ExplainTest < ActiveRecord::SQLite3TestCase + fixtures :developers - def test_explain_for_one_query - explain = Developer.where(:id => 1).explain - assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain - assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) - end + def test_explain_for_one_query + explain = Developer.where(id: 1).explain + assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\?|1)), explain + assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, 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" = ?), explain - assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain - assert_match(/(SCAN )?TABLE audit_logs/, explain) - end - end - end + def test_explain_with_eager_loading + explain = Developer.where(id: 1).includes(:audit_logs).explain + assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\?|1)), explain + assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain + assert_match(/(SCAN )?TABLE audit_logs/, explain) end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 243f65df98..f3ec2b98d3 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -6,11 +6,9 @@ require 'securerandom' module ActiveRecord module ConnectionAdapters class SQLite3Adapter - class QuotingTest < ActiveRecord::TestCase + class QuotingTest < ActiveRecord::SQLite3TestCase def setup - @conn = Base.sqlite3_connection :database => ':memory:', - :adapter => 'sqlite3', - :timeout => 100 + @conn = ActiveRecord::Base.connection end def test_type_cast_binary_encoding_without_logger @@ -89,6 +87,13 @@ module ActiveRecord assert_equal "'hello'", @conn.quote(type.serialize(value)) end + + def test_quoted_time_returns_date_qualified_time + value = ::Time.utc(2000, 1, 1, 12, 30, 0, 999999) + type = Type::Time.new + + assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value)) + end end end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 27f4ba8eb6..bbc9f978bf 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -5,7 +5,7 @@ require 'support/ddl_helper' module ActiveRecord module ConnectionAdapters - class SQLite3AdapterTest < ActiveRecord::TestCase + class SQLite3AdapterTest < ActiveRecord::SQLite3TestCase include DdlHelper self.use_transactional_tests = false @@ -130,11 +130,6 @@ module ActiveRecord assert_equal 'UTF-8', @conn.encoding end - def test_bind_value_substitute - bind_param = @conn.substitute_at('foo') - assert_equal Arel.sql('?'), bind_param.to_sql - end - def test_exec_no_binds with_example_table 'id int, data string' do result = @conn.exec_query('SELECT id, data FROM ex') @@ -218,24 +213,12 @@ module ActiveRecord assert_equal "''", @conn.quote_string("'") end - def test_insert_sql - with_example_table do - 2.times do |i| - rv = @conn.insert_sql "INSERT INTO ex (number) VALUES (#{i})" - assert_equal(i + 1, rv) - end - - records = @conn.execute "SELECT * FROM ex" - assert_equal 2, records.length - end - end - - def test_insert_sql_logged + def test_insert_logged with_example_table do sql = "INSERT INTO ex (number) VALUES (10)" name = "foo" assert_logged [[sql, name, []]] do - @conn.insert_sql sql, name + @conn.insert(sql, name) end end end @@ -244,7 +227,7 @@ module ActiveRecord with_example_table do sql = "INSERT INTO ex (number) VALUES (10)" idval = 'vuvuzela' - id = @conn.insert_sql sql, nil, nil, idval + id = @conn.insert(sql, nil, nil, idval) assert_equal idval, id end end @@ -284,9 +267,9 @@ module ActiveRecord def test_tables with_example_table do - assert_equal %w{ ex }, @conn.tables + ActiveSupport::Deprecation.silence { assert_equal %w{ ex }, @conn.tables } with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer', 'people' do - assert_equal %w{ ex people }.sort, @conn.tables.sort + ActiveSupport::Deprecation.silence { assert_equal %w{ ex people }.sort, @conn.tables.sort } end end end @@ -294,10 +277,12 @@ module ActiveRecord def test_tables_logs_name sql = <<-SQL SELECT name FROM sqlite_master - WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence' + WHERE type IN ('table','view') AND name <> 'sqlite_sequence' SQL assert_logged [[sql.squish, 'SCHEMA', []]] do - @conn.tables('hello') + ActiveSupport::Deprecation.silence do + @conn.tables('hello') + end end end @@ -313,11 +298,12 @@ module ActiveRecord with_example_table do sql = <<-SQL SELECT name FROM sqlite_master - WHERE (type = 'table' OR type = 'view') - AND NOT name = 'sqlite_sequence' AND name = \"ex\" + WHERE type IN ('table','view') AND name <> 'sqlite_sequence' AND name = 'ex' SQL assert_logged [[sql.squish, 'SCHEMA', []]] do - assert @conn.table_exists?('ex') + ActiveSupport::Deprecation.silence do + assert @conn.table_exists?('ex') + end end end end @@ -402,12 +388,6 @@ module ActiveRecord end end - def test_composite_primary_key - with_example_table 'id integer, number integer, foo integer, PRIMARY KEY (id, number)' do - assert_nil @conn.primary_key('ex') - end - end - def test_supports_extensions assert_not @conn.supports_extensions?, 'does not support extensions' end @@ -421,17 +401,20 @@ module ActiveRecord end def test_statement_closed - db = SQLite3::Database.new(ActiveRecord::Base. + db = ::SQLite3::Database.new(ActiveRecord::Base. configurations['arunit']['database']) - statement = SQLite3::Statement.new(db, + statement = ::SQLite3::Statement.new(db, 'CREATE TABLE statement_test (number integer not null)') - statement.stubs(:step).raises(SQLite3::BusyException, 'busy') - statement.stubs(:columns).once.returns([]) - statement.expects(:close).once - SQLite3::Statement.stubs(:new).returns(statement) - - assert_raises ActiveRecord::StatementInvalid do - @conn.exec_query 'select * from statement_test' + statement.stub(:step, ->{ raise ::SQLite3::BusyException.new('busy') }) do + assert_called(statement, :columns, returns: []) do + assert_called(statement, :close) do + ::SQLite3::Statement.stub(:new, statement) do + assert_raises ActiveRecord::StatementInvalid do + @conn.exec_query 'select * from statement_test' + end + end + end + end end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb index deedf67c8e..9b675b804b 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb @@ -3,16 +3,20 @@ require 'models/owner' module ActiveRecord module ConnectionAdapters - class SQLite3CreateFolder < ActiveRecord::TestCase + class SQLite3CreateFolder < ActiveRecord::SQLite3TestCase def test_sqlite_creates_directory Dir.mktmpdir do |dir| - dir = Pathname.new(dir) - @conn = Base.sqlite3_connection :database => dir.join("db/foo.sqlite3"), - :adapter => 'sqlite3', - :timeout => 100 + begin + dir = Pathname.new(dir) + @conn = Base.sqlite3_connection :database => dir.join("db/foo.sqlite3"), + :adapter => 'sqlite3', + :timeout => 100 - assert Dir.exist? dir.join('db') - assert File.exist? dir.join('db/foo.sqlite3') + assert Dir.exist? dir.join('db') + assert File.exist? dir.join('db/foo.sqlite3') + ensure + @conn.disconnect! if @conn + end end end end diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb index fd0044ac05..559b951109 100644 --- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -2,11 +2,11 @@ require 'cases/helper' module ActiveRecord::ConnectionAdapters class SQLite3Adapter - class StatementPoolTest < ActiveRecord::TestCase + class StatementPoolTest < ActiveRecord::SQLite3TestCase if Process.respond_to?(:fork) def test_cache_is_per_pid - cache = StatementPool.new nil, 10 + cache = StatementPool.new(10) cache['foo'] = 'bar' assert_equal 'bar', cache['foo'] @@ -22,4 +22,3 @@ module ActiveRecord::ConnectionAdapters end end end - diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 9d5327bf35..9c99689c1e 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -21,10 +21,10 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::Migration.verbose = @original_verbose end - def test_has_no_primary_key + def test_has_primary_key old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore - assert_nil ActiveRecord::SchemaMigration.primary_key + assert_equal "version", ActiveRecord::SchemaMigration.primary_key ActiveRecord::SchemaMigration.create_table assert_difference "ActiveRecord::SchemaMigration.count", 1 do diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 47fd7345c8..9dadd114a1 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -1,6 +1,5 @@ require 'cases/helper' require 'models/developer' -require 'models/computer' require 'models/project' require 'models/company' require 'models/topic' @@ -19,6 +18,11 @@ require 'models/invoice' require 'models/line_item' require 'models/column' require 'models/record' +require 'models/admin' +require 'models/admin/user' +require 'models/ship' +require 'models/treasure' +require 'models/parrot' class BelongsToAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :topics, @@ -31,6 +35,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal companies(:first_firm).name, firm.name end + def test_missing_attribute_error_is_raised_when_no_foreign_key_attribute + assert_raises(ActiveModel::MissingAttributeError) { Client.select(:id).first.firm } + end + def test_belongs_to_does_not_use_order_by ActiveRecord::SQLCounter.clear_log Client.find(3).firm @@ -45,7 +53,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_belongs_to_with_primary_key_joins_on_correct_column sql = Client.joins(:firm_with_primary_key).to_sql - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) assert_no_match(/`firm_with_primary_keys_companies`\.`id`/, sql) assert_match(/`firm_with_primary_keys_companies`\.`name`/, sql) elsif current_adapter?(:OracleAdapter) @@ -85,7 +93,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end account = model.new - refute account.valid? + assert_not account.valid? assert_equal [{error: :blank}], account.errors.details[:company] ensure ActiveRecord::Base.belongs_to_required_by_default = original_value @@ -102,7 +110,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end account = model.new - refute account.valid? + assert_not account.valid? assert_equal [{error: :blank}], account.errors.details[:company] ensure ActiveRecord::Base.belongs_to_required_by_default = original_value @@ -124,9 +132,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase where("id = :inc", :inc => counter) } - has_many :comments, :class => comments + has_many :comments, :anonymous_class => comments } - belongs_to :post, :class => posts, :inverse_of => false + belongs_to :post, :anonymous_class => posts, :inverse_of => false } assert_equal 0, counter @@ -147,6 +155,30 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::AssociationTypeMismatch) { Account.find(1).firm = Project.find(1) } end + def test_raises_type_mismatch_with_namespaced_class + assert_nil defined?(Region), "This test requires that there is no top-level Region class" + + ActiveRecord::Base.connection.instance_eval do + create_table(:admin_regions) { |t| t.string :name } + add_column :admin_users, :region_id, :integer + end + Admin.const_set "RegionalUser", Class.new(Admin::User) { belongs_to(:region) } + Admin.const_set "Region", Class.new(ActiveRecord::Base) + + e = assert_raise(ActiveRecord::AssociationTypeMismatch) { + Admin::RegionalUser.new(region: 'wrong value') + } + assert_match(/^Region\([^)]+\) expected, got "wrong value" which is an instance of String\([^)]+\)$/, e.message) + ensure + Admin.send :remove_const, "Region" if Admin.const_defined?("Region") + Admin.send :remove_const, "RegionalUser" if Admin.const_defined?("RegionalUser") + + ActiveRecord::Base.connection.instance_eval do + remove_column :admin_users, :region_id if column_exists?(:admin_users, :region_id) + drop_table :admin_regions, if_exists: true + end + end + def test_natural_assignment apple = Firm.create("name" => "Apple") citibank = Account.create("credit_limit" => 10) @@ -263,7 +295,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase client = Client.find(3) client.firm = nil client.save - assert_nil client.firm(true) + client.association(:firm).reload + assert_nil client.firm assert_nil client.client_of end @@ -271,7 +304,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name) client.firm_with_primary_key = nil client.save - assert_nil client.firm_with_primary_key(true) + client.association(:firm_with_primary_key).reload + assert_nil client.firm_with_primary_key assert_nil client.client_of end @@ -288,9 +322,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_polymorphic_association_class sponsor = Sponsor.new assert_nil sponsor.association(:sponsorable).send(:klass) + sponsor.association(:sponsorable).reload + assert_nil sponsor.sponsorable sponsor.sponsorable_type = '' # the column doesn't have to be declared NOT NULL assert_nil sponsor.association(:sponsorable).send(:klass) + sponsor.association(:sponsorable).reload + assert_nil sponsor.sponsorable sponsor.sponsorable = Member.new :name => "Bert" assert_equal Member, sponsor.association(:sponsorable).send(:klass) @@ -311,6 +349,22 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Company.all.merge!(:includes => :firm_with_select ).find(2).firm_with_select.attributes.size end + def test_belongs_to_without_counter_cache_option + # Ship has a conventionally named `treasures_count` column, but the counter_cache + # option is not given on the association. + ship = Ship.create(name: 'Countless') + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do + treasure = Treasure.new(name: 'Gold', ship: ship) + treasure.save + end + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed unless counter_cache is given on the relation" do + treasure = ship.treasures.first + treasure.destroy + end + end + def test_belongs_to_counter debate = Topic.create("title" => "debate") assert_equal 0, debate.read_attribute("replies_count"), "No replies yet" @@ -422,13 +476,33 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_queries(1) { line_item.touch } end + def test_belongs_to_with_touch_on_multiple_records + line_item = LineItem.create!(amount: 1) + line_item2 = LineItem.create!(amount: 2) + Invoice.create!(line_items: [line_item, line_item2]) + + assert_queries(1) do + LineItem.transaction do + line_item.touch + line_item2.touch + end + end + + assert_queries(2) do + line_item.touch + line_item2.touch + end + end + def test_belongs_to_with_touch_option_on_touch_without_updated_at_attributes assert_not LineItem.column_names.include?("updated_at") line_item = LineItem.create! invoice = Invoice.create!(line_items: [line_item]) initial = invoice.updated_at - line_item.touch + travel(1.second) do + line_item.touch + end assert_not_equal initial, invoice.reload.updated_at end @@ -507,7 +581,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert final_cut.persisted? assert firm.persisted? assert_equal firm, final_cut.firm - assert_equal firm, final_cut.firm(true) + final_cut.association(:firm).reload + assert_equal firm, final_cut.firm end def test_assignment_before_child_saved_with_primary_key @@ -519,7 +594,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert final_cut.persisted? assert firm.persisted? assert_equal firm, final_cut.firm_with_primary_key - assert_equal firm, final_cut.firm_with_primary_key(true) + final_cut.association(:firm_with_primary_key).reload + assert_equal firm, final_cut.firm_with_primary_key end def test_new_record_with_foreign_key_but_no_object @@ -624,6 +700,17 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 17, reply.replies.size end + def test_replace_counter_cache + topic = Topic.create(title: "Zoom-zoom-zoom") + reply = Reply.create(title: "re: zoom", content: "speedy quick!") + + reply.topic = topic + reply.save + topic.reload + + assert_equal 1, topic.replies_count + end + def test_association_assignment_sticks post = Post.first @@ -650,7 +737,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert companies(:first_client).readonly_firm.readonly? end - def test_test_polymorphic_assignment_foreign_key_type_string + def test_polymorphic_assignment_foreign_key_type_string comment = Comment.first comment.author = Author.first comment.resource = Member.first @@ -1014,6 +1101,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase Column.create! record: record assert_equal 1, Column.count end + + def test_association_force_reload_with_only_true_is_deprecated + client = Client.find(3) + + assert_deprecated { client.firm(true) } + end end class BelongsToWithForeignKeyTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb b/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb new file mode 100644 index 0000000000..2b867965ba --- /dev/null +++ b/activerecord/test/cases/associations/bidirectional_destroy_dependencies_test.rb @@ -0,0 +1,41 @@ +require 'cases/helper' +require 'models/content' + +class BidirectionalDestroyDependenciesTest < ActiveRecord::TestCase + fixtures :content, :content_positions + + def setup + Content.destroyed_ids.clear + ContentPosition.destroyed_ids.clear + end + + def test_bidirectional_dependence_when_destroying_item_with_belongs_to_association + content_position = ContentPosition.find(1) + content = content_position.content + assert_not_nil content + + content_position.destroy + + assert_equal [content_position.id], ContentPosition.destroyed_ids + assert_equal [content.id], Content.destroyed_ids + end + + def test_bidirectional_dependence_when_destroying_item_with_has_one_association + content = Content.find(1) + content_position = content.content_position + assert_not_nil content_position + + content.destroy + + assert_equal [content.id], Content.destroyed_ids + assert_equal [content_position.id], ContentPosition.destroyed_ids + end + + def test_bidirectional_dependence_when_destroying_item_with_has_one_association_fails_first_time + content = ContentWhichRequiresTwoDestroyCalls.find(1) + + 2.times { content.destroy } + + assert_equal content.destroyed?, true + end +end diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index a531e0e02c..a4298a25a6 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -177,14 +177,14 @@ class AssociationCallbacksTest < ActiveRecord::TestCase end def test_dont_add_if_before_callback_raises_exception - assert !@david.unchangable_posts.include?(@authorless) + assert !@david.unchangeable_posts.include?(@authorless) begin - @david.unchangable_posts << @authorless + @david.unchangeable_posts << @authorless rescue Exception end assert @david.post_log.empty? - assert !@david.unchangable_posts.include?(@authorless) + assert !@david.unchangeable_posts.include?(@authorless) @david.reload - assert !@david.unchangable_posts.include?(@authorless) + assert !@david.unchangeable_posts.include?(@authorless) end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 7d8b933992..7f2a2229ee 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -24,6 +24,8 @@ require 'models/membership' require 'models/club' require 'models/categorization' require 'models/sponsor' +require 'models/mentor' +require 'models/contract' class EagerAssociationTest < ActiveRecord::TestCase fixtures :posts, :comments, :authors, :essays, :author_addresses, :categories, :categories_posts, @@ -108,53 +110,57 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle - Comment.connection.expects(:in_clause_length).at_least_once.returns(5) - posts = Post.all.merge!(:includes=>:comments).to_a - assert_equal 11, posts.size + assert_called(Comment.connection, :in_clause_length, returns: 5) do + posts = Post.all.merge!(:includes=>:comments).to_a + assert_equal 11, posts.size + end end def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle - Comment.connection.expects(:in_clause_length).at_least_once.returns(nil) - posts = Post.all.merge!(:includes=>:comments).to_a - assert_equal 11, posts.size + assert_called(Comment.connection, :in_clause_length, returns: nil) do + posts = Post.all.merge!(:includes=>:comments).to_a + assert_equal 11, posts.size + end end def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle - Comment.connection.expects(:in_clause_length).at_least_once.returns(5) - posts = Post.all.merge!(:includes=>:categories).to_a - assert_equal 11, posts.size + assert_called(Comment.connection, :in_clause_length, times: 2, returns: 5) do + posts = Post.all.merge!(:includes=>:categories).to_a + assert_equal 11, posts.size + end end def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle - Comment.connection.expects(:in_clause_length).at_least_once.returns(nil) - posts = Post.all.merge!(:includes=>:categories).to_a - assert_equal 11, posts.size + assert_called(Comment.connection, :in_clause_length, times: 2, returns: nil) do + posts = Post.all.merge!(:includes=>:categories).to_a + assert_equal 11, posts.size + end end def test_load_associated_records_in_one_query_when_adapter_has_no_limit - Comment.connection.expects(:in_clause_length).at_least_once.returns(nil) - - post = posts(:welcome) - assert_queries(2) do - Post.includes(:comments).where(:id => post.id).to_a + assert_called(Comment.connection, :in_clause_length, returns: nil) do + post = posts(:welcome) + assert_queries(2) do + Post.includes(:comments).where(:id => post.id).to_a + end end end def test_load_associated_records_in_several_queries_when_many_ids_passed - Comment.connection.expects(:in_clause_length).at_least_once.returns(1) - - post1, post2 = posts(:welcome), posts(:thinking) - assert_queries(3) do - Post.includes(:comments).where(:id => [post1.id, post2.id]).to_a + assert_called(Comment.connection, :in_clause_length, returns: 1) do + post1, post2 = posts(:welcome), posts(:thinking) + assert_queries(3) do + Post.includes(:comments).where(:id => [post1.id, post2.id]).to_a + end end end def test_load_associated_records_in_one_query_when_a_few_ids_passed - Comment.connection.expects(:in_clause_length).at_least_once.returns(3) - - post = posts(:welcome) - assert_queries(2) do - Post.includes(:comments).where(:id => post.id).to_a + assert_called(Comment.connection, :in_clause_length, returns: 3) do + post = posts(:welcome) + assert_queries(2) do + Post.includes(:comments).where(:id => post.id).to_a + end end end @@ -743,6 +749,38 @@ class EagerAssociationTest < ActiveRecord::TestCase } end + def test_eager_has_many_through_with_order + tag = OrderedTag.create(name: 'Foo') + post1 = Post.create!(title: 'Beaches', body: "I like beaches!") + post2 = Post.create!(title: 'Pools', body: "I like pools!") + + Tagging.create!(taggable_type: 'Post', taggable_id: post1.id, tag: tag) + Tagging.create!(taggable_type: 'Post', taggable_id: post2.id, tag: tag) + + tag_with_includes = OrderedTag.includes(:tagged_posts).find(tag.id) + assert_equal(tag_with_includes.taggings.map(&:taggable).map(&:title), tag_with_includes.tagged_posts.map(&:title)) + end + + def test_eager_has_many_through_multiple_with_order + tag1 = OrderedTag.create!(name: 'Bar') + tag2 = OrderedTag.create!(name: 'Foo') + + post1 = Post.create!(title: 'Beaches', body: "I like beaches!") + post2 = Post.create!(title: 'Pools', body: "I like pools!") + + Tagging.create!(taggable: post1, tag: tag1) + Tagging.create!(taggable: post2, tag: tag1) + Tagging.create!(taggable: post2, tag: tag2) + Tagging.create!(taggable: post1, tag: tag2) + + tags_with_includes = OrderedTag.where(id: [tag1, tag2].map(&:id)).includes(:tagged_posts).order(:id).to_a + tag1_with_includes = tags_with_includes.first + tag2_with_includes = tags_with_includes.last + + assert_equal([post2, post1].map(&:title), tag1_with_includes.tagged_posts.map(&:title)) + assert_equal([post1, post2].map(&:title), tag2_with_includes.tagged_posts.map(&:title)) + end + def test_eager_with_default_scope developer = EagerDeveloperWithDefaultScope.where(:name => 'David').first projects = Project.order(:id).to_a @@ -759,6 +797,23 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + def test_eager_with_default_scope_as_class_method_using_find_method + david = developers(:david) + developer = EagerDeveloperWithClassMethodDefaultScope.find(david.id) + projects = Project.order(:id).to_a + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + + def test_eager_with_default_scope_as_class_method_using_find_by_method + developer = EagerDeveloperWithClassMethodDefaultScope.find_by(name: 'David') + projects = Project.order(:id).to_a + assert_no_queries do + assert_equal(projects, developer.projects) + end + end + def test_eager_with_default_scope_as_lambda developer = EagerDeveloperWithLambdaDefaultScope.where(:name => 'David').first projects = Project.order(:id).to_a @@ -1150,12 +1205,30 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_no_queries { assert client.accounts.empty? } end - def test_preloading_has_many_through_with_uniq + def test_preloading_has_many_through_with_distinct mary = Author.includes(:unique_categorized_posts).where(:id => authors(:mary).id).first assert_equal 1, mary.unique_categorized_posts.length assert_equal 1, mary.unique_categorized_post_ids.length end + def test_preloading_has_one_using_reorder + klass = Class.new(ActiveRecord::Base) do + def self.name; "TempAuthor"; end + self.table_name = "authors" + has_one :post, class_name: "PostWithDefaultScope", foreign_key: :author_id + has_one :reorderd_post, -> { reorder(title: :desc) }, class_name: "PostWithDefaultScope", foreign_key: :author_id + end + + author = klass.first + # PRECONDITION: make sure ordering results in different results + assert_not_equal author.post, author.reorderd_post + + preloaded_reorderd_post = klass.preload(:reorderd_post).first.reorderd_post + + assert_equal author.reorderd_post, preloaded_reorderd_post + assert_equal Post.order(title: :desc).first.title, preloaded_reorderd_post.title + end + def test_preloading_polymorphic_with_custom_foreign_type sponsor = sponsors(:moustache_club_sponsor_for_groucho) groucho = members(:groucho) @@ -1175,17 +1248,11 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_join_eager_with_empty_order_should_generate_valid_sql - assert_nothing_raised(ActiveRecord::StatementInvalid) do + assert_nothing_raised do Post.includes(:comments).order("").where(:comments => {:body => "Thank you for the welcome"}).first end end - def test_join_eager_with_nil_order_should_generate_valid_sql - assert_nothing_raised(ActiveRecord::StatementInvalid) do - Post.includes(:comments).order(nil).where(:comments => {:body => "Thank you for the welcome"}).first - end - end - def test_deep_including_through_habtm # warm up habtm cache posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a @@ -1197,6 +1264,16 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_no_queries { assert_equal 2, posts[1].categories[0].categorizations.length } end + def test_eager_load_multiple_associations_with_references + mentor = Mentor.create!(name: "Barış Can DAYLIK") + developer = Developer.create!(name: "Mehmet Emin İNAÇ", mentor: mentor) + Contract.create!(developer: developer) + project = Project.create!(name: "VNGRS", mentor: mentor) + project.developers << developer + projects = Project.references(:mentors).includes(mentor: { developers: :contracts }, developers: :contracts) + assert_equal projects.last.mentor.developers.first.contracts, projects.last.developers.last.contracts + end + test "scoping with a circular preload" do assert_equal Comment.find(1), Comment.preload(:post => :comments).scoping { Comment.find(1) } end @@ -1308,6 +1385,26 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_match message, error.message end + test "preload with invalid argument" do + exception = assert_raises(ArgumentError) do + Author.preload(10).to_a + end + assert_equal('10 was not recognized for preload', exception.message) + end + + test "associations with extensions are not instance dependent" do + assert_nothing_raised do + Author.includes(:posts_with_extension).to_a + end + end + + test "including associations with extensions and an instance dependent scope is not supported" do + e = assert_raises(ArgumentError) do + Author.includes(:posts_with_extension_and_instance).to_a + end + assert_match(/Preloading instance dependent scopes is not supported/, e.message) + end + test "preloading readonly association" do # has-one firm = Firm.where(id: "1").preload(:readonly_account).first! @@ -1349,4 +1446,10 @@ class EagerAssociationTest < ActiveRecord::TestCase post = Post.eager_load(:tags).where('tags.name = ?', 'General').first assert_equal posts(:welcome), post end + + # CollectionProxy#reader is expensive, so the preloader avoids calling it. + test "preloading has_many_through association avoids calling association.reader" do + ActiveRecord::Associations::HasManyAssociation.any_instance.expects(:reader).never + Author.preload(:readonly_comments).first! + end end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index aea9207bfe..1bbca84bb2 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -3,6 +3,7 @@ require 'models/developer' require 'models/computer' require 'models/project' require 'models/company' +require 'models/course' require 'models/customer' require 'models/order' require 'models/categorization' @@ -14,6 +15,7 @@ require 'models/tagging' require 'models/parrot' require 'models/person' require 'models/pirate' +require 'models/professor' require 'models/treasure' require 'models/price_estimate' require 'models/club' @@ -83,6 +85,25 @@ class DeveloperWithSymbolClassName < Developer has_and_belongs_to_many :projects, class_name: :ProjectWithSymbolsForKeys end +class DeveloperWithExtendOption < Developer + module NamedExtension + def category + 'sns' + end + end + + has_and_belongs_to_many :projects, extend: NamedExtension +end + +class ProjectUnscopingDavidDefaultScope < ActiveRecord::Base + self.table_name = 'projects' + has_and_belongs_to_many :developers, -> { unscope(where: 'name') }, + class_name: "LazyBlockDeveloperCalledDavid", + join_table: "developers_projects", + foreign_key: "project_id", + association_foreign_key: "developer_id" +end + class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings, :computers @@ -125,6 +146,19 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, country.treaties.count end + def test_join_table_composite_primary_key_should_not_warn + country = Country.new(:name => 'India') + country.country_id = 'c1' + country.save! + + treaty = Treaty.new(:name => 'peace') + treaty.treaty_id = 't1' + warning = capture(:stderr) do + country.treaties << treaty + end + assert_no_match(/WARNING: Rails does not support composite primary key\./, warning) + end + def test_has_and_belongs_to_many david = Developer.find(1) @@ -147,8 +181,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase jamis.projects << action_controller assert_equal 2, jamis.projects.size - assert_equal 2, jamis.projects(true).size - assert_equal 2, action_controller.developers(true).size + assert_equal 2, jamis.projects.reload.size + assert_equal 2, action_controller.developers.reload.size end def test_adding_type_mismatch @@ -166,9 +200,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase action_controller.developers << jamis - assert_equal 2, jamis.projects(true).size + assert_equal 2, jamis.projects.reload.size assert_equal 2, action_controller.developers.size - assert_equal 2, action_controller.developers(true).size + assert_equal 2, action_controller.developers.reload.size end def test_adding_from_the_project_fixed_timestamp @@ -182,9 +216,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase action_controller.developers << jamis assert_equal updated_at, jamis.updated_at - assert_equal 2, jamis.projects(true).size + assert_equal 2, jamis.projects.reload.size assert_equal 2, action_controller.developers.size - assert_equal 2, action_controller.developers(true).size + assert_equal 2, action_controller.developers.reload.size end def test_adding_multiple @@ -193,7 +227,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase aredridel.projects.reload aredridel.projects.push(Project.find(1), Project.find(2)) assert_equal 2, aredridel.projects.size - assert_equal 2, aredridel.projects(true).size + assert_equal 2, aredridel.projects.reload.size end def test_adding_a_collection @@ -202,7 +236,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase aredridel.projects.reload aredridel.projects.concat([Project.find(1), Project.find(2)]) assert_equal 2, aredridel.projects.size - assert_equal 2, aredridel.projects(true).size + assert_equal 2, aredridel.projects.reload.size end def test_habtm_adding_before_save @@ -217,7 +251,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal no_of_devels+1, Developer.count assert_equal no_of_projects+1, Project.count assert_equal 2, aredridel.projects.size - assert_equal 2, aredridel.projects(true).size + assert_equal 2, aredridel.projects.reload.size end def test_habtm_saving_multiple_relationships @@ -234,7 +268,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal developers, new_project.developers end - def test_habtm_unique_order_preserved + def test_habtm_distinct_order_preserved assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).non_unique_developers assert_equal developers(:poor_jamis, :jamis, :david), projects(:active_record).developers end @@ -339,7 +373,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 'Yet Another Testing Title', another_post.title end - def test_uniq_after_the_fact + def test_distinct_after_the_fact dev = developers(:jamis) dev.projects << projects(:active_record) dev.projects << projects(:active_record) @@ -348,13 +382,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, dev.projects.distinct.size end - def test_uniq_before_the_fact + def test_distinct_before_the_fact projects(:active_record).developers << developers(:jamis) projects(:active_record).developers << developers(:david) assert_equal 3, projects(:active_record, :reload).developers.size end - def test_uniq_option_prevents_duplicate_push + def test_distinct_option_prevents_duplicate_push project = projects(:active_record) project.developers << developers(:jamis) project.developers << developers(:david) @@ -365,7 +399,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 3, project.developers.size end - def test_uniq_when_association_already_loaded + def test_distinct_when_association_already_loaded project = projects(:active_record) project.developers << [ developers(:jamis), developers(:david), developers(:jamis), developers(:david) ] assert_equal 3, Project.includes(:developers).find(project.id).developers.size @@ -381,8 +415,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase david.projects.delete(active_record) assert_equal 1, david.projects.size - assert_equal 1, david.projects(true).size - assert_equal 2, active_record.developers(true).size + assert_equal 1, david.projects.reload.size + assert_equal 2, active_record.developers.reload.size end def test_deleting_array @@ -390,7 +424,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase david.projects.reload david.projects.delete(Project.all.to_a) assert_equal 0, david.projects.size - assert_equal 0, david.projects(true).size + assert_equal 0, david.projects.reload.size end def test_deleting_all @@ -398,7 +432,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase david.projects.reload david.projects.clear assert_equal 0, david.projects.size - assert_equal 0, david.projects(true).size + assert_equal 0, david.projects.reload.size end def test_removing_associations_on_destroy @@ -424,7 +458,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert join_records.empty? assert_equal 1, david.reload.projects.size - assert_equal 1, david.projects(true).size + assert_equal 1, david.projects.reload.size end def test_destroying_many @@ -440,7 +474,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert join_records.empty? assert_equal 0, david.reload.projects.size - assert_equal 0, david.projects(true).size + assert_equal 0, david.projects.reload.size end def test_destroy_all @@ -456,7 +490,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert join_records.empty? assert david.projects.empty? - assert david.projects(true).empty? + assert david.projects.reload.empty? end def test_destroy_associations_destroys_multiple_associations @@ -472,11 +506,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase join_records = Parrot.connection.select_all("SELECT * FROM parrots_pirates WHERE parrot_id = #{george.id}") assert join_records.empty? - assert george.pirates(true).empty? + assert george.pirates.reload.empty? join_records = Parrot.connection.select_all("SELECT * FROM parrots_treasures WHERE parrot_id = #{george.id}") assert join_records.empty? - assert george.treasures(true).empty? + assert george.treasures.reload.empty? end def test_associations_with_conditions @@ -577,6 +611,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal developers(:poor_jamis), projects(:active_record).developers.where("salary < 10000").first end + def test_association_with_extend_option + eponine = DeveloperWithExtendOption.create(name: 'Eponine') + assert_equal 'sns', eponine.projects.category + end + def test_replace_with_less david = developers(:david) david.projects = [projects(:action_controller)] @@ -639,7 +678,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_habtm_respects_select - categories(:technology).select_testing_posts(true).each do |o| + categories(:technology).select_testing_posts.reload.each do |o| assert_respond_to o, :correctness_marker end assert_respond_to categories(:technology).select_testing_posts.first, :correctness_marker @@ -711,7 +750,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_loaded_associations developer = developers(:david) - developer.projects(true) + developer.projects.reload assert_queries(0) do developer.project_ids developer.project_ids @@ -779,9 +818,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_association_proxy_transaction_method_starts_transaction_in_association_class - Post.expects(:transaction) - Category.first.posts.transaction do - # nothing + assert_called(Post, :transaction) do + Category.first.posts.transaction do + # nothing + end end end @@ -800,12 +840,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_no_queries { david.projects.columns } end - def test_attributes_are_being_set_when_initialized_from_habm_association_with_where_clause + def test_attributes_are_being_set_when_initialized_from_habtm_association_with_where_clause new_developer = projects(:action_controller).developers.where(:name => "Marcelo").build assert_equal new_developer.name, "Marcelo" end - def test_attributes_are_being_set_when_initialized_from_habm_association_with_multiple_where_clauses + def test_attributes_are_being_set_when_initialized_from_habtm_association_with_multiple_where_clauses new_developer = projects(:action_controller).developers.where(:name => "Marcelo").where(:salary => 90_000).build assert_equal new_developer.name, "Marcelo" assert_equal new_developer.salary, 90_000 @@ -898,8 +938,61 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_with_symbol_class_name - assert_nothing_raised NoMethodError do + assert_nothing_raised do DeveloperWithSymbolClassName.new end end + + def test_association_force_reload_with_only_true_is_deprecated + developer = Developer.find(1) + + assert_deprecated { developer.projects(true) } + end + + def test_alternate_database + professor = Professor.create(name: "Plum") + course = Course.create(name: "Forensics") + assert_equal 0, professor.courses.count + assert_nothing_raised do + professor.courses << course + end + assert_equal 1, professor.courses.count + end + + def test_habtm_scope_can_unscope + project = ProjectUnscopingDavidDefaultScope.new + project.save! + + developer = LazyBlockDeveloperCalledDavid.new(name: "Not David") + developer.save! + project.developers << developer + + projects = ProjectUnscopingDavidDefaultScope.includes(:developers).where(id: project.id) + assert_equal 1, projects.first.developers.size + end + + def test_preloaded_associations_size + assert_equal Project.first.salaried_developers.size, + Project.preload(:salaried_developers).first.salaried_developers.size + + assert_equal Project.includes(:salaried_developers).references(:salaried_developers).first.salaried_developers.size, + Project.preload(:salaried_developers).first.salaried_developers.size + + # Nested HATBM + first_project = Developer.first.projects.first + preloaded_first_project = + Developer.preload(projects: :salaried_developers). + first. + projects. + detect { |p| p.id == first_project.id } + + assert preloaded_first_project.salaried_developers.loaded?, true + assert_equal first_project.salaried_developers.size, preloaded_first_project.salaried_developers.size + end + + def test_has_and_belongs_to_many_is_useable_with_belongs_to_required_by_default + assert_difference "Project.first.developers_required_by_default.size", 1 do + Project.first.developers_required_by_default.create!(name: "Sean", salary: 50000) + end + 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..e975f4fbdd 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -31,6 +31,8 @@ require 'models/student' require 'models/pirate' require 'models/ship' require 'models/ship_part' +require 'models/treasure' +require 'models/parrot' require 'models/tyre' require 'models/subscriber' require 'models/subscription' @@ -119,9 +121,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase developer_project = Class.new(ActiveRecord::Base) { self.table_name = 'developers_projects' - belongs_to :developer, :class => dev + belongs_to :developer, :anonymous_class => dev } - has_many :developer_projects, :class => developer_project, :foreign_key => 'developer_id' + has_many :developer_projects, :anonymous_class => developer_project, :foreign_key => 'developer_id' } dev = developer.first named = Developer.find(dev.id) @@ -140,13 +142,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase comments = Class.new(ActiveRecord::Base) { self.table_name = 'comments' self.inheritance_column = 'not_there' - belongs_to :post, :class => post + belongs_to :post, :anonymous_class => post default_scope -> { counter += 1 where("id = :inc", :inc => counter) } } - has_many :comments, :class => comments, :foreign_key => 'post_id' + has_many :comments, :anonymous_class => comments, :foreign_key => 'post_id' } assert_equal 0, counter post = posts.first @@ -168,7 +170,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase part = ShipPart.create(name: 'cockpit') updated_at = part.updated_at - ship.parts << part + travel(1.second) do + ship.parts << part + end assert_equal part.ship, ship assert_not_equal part.updated_at, updated_at @@ -199,9 +203,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase bulb = car.bulbs.create assert_equal 'defaulty', bulb.name + end + + def test_build_and_create_from_association_should_respect_passed_attributes_over_default_scope + car = Car.create(name: 'honda') + + bulb = car.bulbs.build(name: 'exotic') + assert_equal 'exotic', bulb.name - bulb = car.bulbs.create(:name => 'exotic') + bulb = car.bulbs.create(name: 'exotic') assert_equal 'exotic', bulb.name + + bulb = car.awesome_bulbs.build(frickinawesome: false) + assert_equal false, bulb.frickinawesome + + bulb = car.awesome_bulbs.create(frickinawesome: false) + assert_equal false, bulb.frickinawesome end def test_build_from_association_should_respect_scope @@ -391,6 +408,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_no_queries do + bulbs.third_to_last() + bulbs.third_to_last({}) + end + + assert_no_queries do + bulbs.second_to_last() + bulbs.second_to_last({}) + end + + assert_no_queries do bulbs.last() bulbs.last({}) end @@ -704,7 +731,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase natural = Client.new("name" => "Natural Company") companies(:first_firm).clients_of_firm << natural assert_equal 3, companies(:first_firm).clients_of_firm.size # checking via the collection - assert_equal 3, companies(:first_firm).clients_of_firm(true).size # checking using the db + assert_equal 3, companies(:first_firm).clients_of_firm.reload.size # checking using the db assert_equal natural, companies(:first_firm).clients_of_firm.last end @@ -759,7 +786,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")]) assert_equal 4, companies(:first_firm).clients_of_firm.size - assert_equal 4, companies(:first_firm).clients_of_firm(true).size + assert_equal 4, companies(:first_firm).clients_of_firm.reload.size end def test_transactions_when_adding_to_persisted @@ -771,7 +798,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase rescue Client::RaisedOnSave end - assert !companies(:first_firm).clients_of_firm(true).include?(good) + assert !companies(:first_firm).clients_of_firm.reload.include?(good) end def test_transactions_when_adding_to_new_record @@ -903,12 +930,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client") assert new_client.persisted? assert_equal new_client, companies(:first_firm).clients_of_firm.last - assert_equal new_client, companies(:first_firm).clients_of_firm(true).last + assert_equal new_client, companies(:first_firm).clients_of_firm.reload.last end def test_create_many companies(:first_firm).clients_of_firm.create([{"name" => "Another Client"}, {"name" => "Another Client II"}]) - assert_equal 4, companies(:first_firm).clients_of_firm(true).size + assert_equal 4, companies(:first_firm).clients_of_firm.reload.size end def test_create_followed_by_save_does_not_load_target @@ -921,7 +948,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first) assert_equal 1, companies(:first_firm).clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.reload.size end def test_deleting_before_save @@ -932,6 +959,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 0, new_firm.clients_of_firm.size end + def test_has_many_without_counter_cache_option + # Ship has a conventionally named `treasures_count` column, but the counter_cache + # option is not given on the association. + ship = Ship.create(name: 'Countless', treasures_count: 10) + + assert_not Ship.reflect_on_association(:treasures).has_cached_counter? + + # Count should come from sql count() of treasures rather than treasures_count attribute + assert_equal ship.treasures.size, 0 + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do + ship.treasures.create(name: 'Gold') + end + + assert_no_difference lambda { ship.reload.treasures_count }, "treasures_count should not be changed" do + ship.treasures.destroy_all + end + end + def test_deleting_updates_counter_cache topic = Topic.order("id ASC").first assert_equal topic.replies.to_a.size, topic.replies_count @@ -1058,7 +1104,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 3, companies(:first_firm).clients_of_firm.size companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1], companies(:first_firm).clients_of_firm[2]]) assert_equal 0, companies(:first_firm).clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + assert_equal 0, companies(:first_firm).clients_of_firm.reload.size end def test_delete_all @@ -1079,7 +1125,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase companies(:first_firm).clients_of_firm.reset companies(:first_firm).clients_of_firm.delete_all assert_equal 0, companies(:first_firm).clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + assert_equal 0, companies(:first_firm).clients_of_firm.reload.size end def test_transaction_when_deleting_persisted @@ -1093,7 +1139,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase rescue Client::RaisedOnDestroy end - assert_equal [good, bad], companies(:first_firm).clients_of_firm(true) + assert_equal [good, bad], companies(:first_firm).clients_of_firm.reload end def test_transaction_when_deleting_new_record @@ -1113,7 +1159,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients_of_firm.clear assert_equal 0, firm.clients_of_firm.size - assert_equal 0, firm.clients_of_firm(true).size + assert_equal 0, firm.clients_of_firm.reload.size assert_equal [], Client.destroyed_client_ids[firm.id] # Should not be destroyed since the association is not dependent. @@ -1149,7 +1195,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.dependent_clients_of_firm.clear assert_equal 0, firm.dependent_clients_of_firm.size - assert_equal 0, firm.dependent_clients_of_firm(true).size + assert_equal 0, firm.dependent_clients_of_firm.reload.size assert_equal [], Client.destroyed_client_ids[firm.id] # Should be destroyed since the association is dependent. @@ -1182,7 +1228,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.exclusively_dependent_clients_of_firm.clear assert_equal 0, firm.exclusively_dependent_clients_of_firm.size - assert_equal 0, firm.exclusively_dependent_clients_of_firm(true).size + assert_equal 0, firm.exclusively_dependent_clients_of_firm.reload.size # no destroy-filters should have been called assert_equal [], Client.destroyed_client_ids[firm.id] @@ -1231,7 +1277,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase # break the vanilla firm_id foreign key assert_equal 3, firm.clients.count firm.clients.first.update_columns(firm_id: nil) - assert_equal 2, firm.clients(true).count + assert_equal 2, firm.clients.reload.count assert_equal 2, firm.clients_using_primary_key_with_delete_all.count old_record = firm.clients_using_primary_key_with_delete_all.first firm = Firm.first @@ -1257,7 +1303,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients_of_firm.clear assert_equal 0, firm.clients_of_firm.size - assert_equal 0, firm.clients_of_firm(true).size + assert_equal 0, firm.clients_of_firm.reload.size end def test_deleting_a_item_which_is_not_in_the_collection @@ -1265,7 +1311,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase summit = Client.find_by_name('Summit') companies(:first_firm).clients_of_firm.delete(summit) assert_equal 2, companies(:first_firm).clients_of_firm.size - assert_equal 2, companies(:first_firm).clients_of_firm(true).size + assert_equal 2, companies(:first_firm).clients_of_firm.reload.size assert_equal 2, summit.client_of end @@ -1303,7 +1349,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_equal 1, companies(:first_firm).reload.clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.reload.size end def test_destroying_by_fixnum_id @@ -1314,7 +1360,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_equal 1, companies(:first_firm).reload.clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.reload.size end def test_destroying_by_string_id @@ -1325,7 +1371,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_equal 1, companies(:first_firm).reload.clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.reload.size end def test_destroying_a_collection @@ -1338,7 +1384,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_equal 1, companies(:first_firm).reload.clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.reload.size end def test_destroy_all @@ -1349,7 +1395,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal clients.sort_by(&:id), destroyed.sort_by(&:id) assert destroyed.all?(&:frozen?), "destroyed clients should be frozen" assert companies(:first_firm).clients_of_firm.empty?, "37signals has no clients after destroy all" - assert companies(:first_firm).clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh" + assert companies(:first_firm).clients_of_firm.reload.empty?, "37signals has no clients after destroy all and refresh" end def test_dependence @@ -1426,6 +1472,26 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert firm.companies.exists?(:name => 'child') end + def test_restrict_with_error_is_deprecated_using_key_many + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations :en, activerecord: { errors: { messages: { restrict_dependent_destroy: { many: 'message for deprecated key' } } } } + + firm = RestrictedWithErrorFirm.create!(name: 'restrict') + firm.companies.create(name: 'child') + + assert !firm.companies.empty? + + assert_deprecated { firm.destroy } + + assert !firm.errors.empty? + + assert_equal 'message for deprecated key', firm.errors[:base].first + assert RestrictedWithErrorFirm.exists?(name: 'restrict') + assert firm.companies.exists?(name: 'child') + ensure + I18n.backend.reload! + end + def test_restrict_with_error firm = RestrictedWithErrorFirm.create!(:name => 'restrict') firm.companies.create(:name => 'child') @@ -1441,6 +1507,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert firm.companies.exists?(:name => 'child') end + def test_restrict_with_error_with_locale + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations 'en', activerecord: {attributes: {restricted_with_error_firm: {companies: 'client companies'}}} + firm = RestrictedWithErrorFirm.create!(name: 'restrict') + firm.companies.create(name: 'child') + + assert !firm.companies.empty? + + firm.destroy + + assert !firm.errors.empty? + + assert_equal "Cannot delete record because dependent client companies exist", firm.errors[:base].first + assert RestrictedWithErrorFirm.exists?(name: 'restrict') + assert firm.companies.exists?(name: 'child') + ensure + I18n.backend.reload! + end + def test_included_in_collection assert_equal true, companies(:first_firm).clients.include?(Client.find(2)) end @@ -1503,6 +1588,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 @@ -1516,7 +1603,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase rescue Client::RaisedOnSave end - assert_equal [good], companies(:first_firm).clients_of_firm(true) + assert_equal [good], companies(:first_firm).clients_of_firm.reload end def test_transactions_when_replacing_on_new_record @@ -1532,7 +1619,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_loaded_associations company = companies(:first_firm) - company.clients(true) + company.clients.reload assert_queries(0) do company.client_ids company.client_ids @@ -1586,7 +1673,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.client_ids = [companies(:first_client).id, nil, companies(:second_client).id, ''] firm.save! - assert_equal 2, firm.clients(true).size + assert_equal 2, firm.clients.reload.size assert_equal true, firm.clients.include?(companies(:second_client)) end @@ -2117,6 +2204,26 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [bulb1, bulb2], car.all_bulbs.sort_by(&:id) end + test "can unscope and where the default scope of the associated model" do + Car.has_many :other_bulbs, -> { unscope(where: [:name]).where(name: 'other') }, class_name: "Bulb" + car = Car.create! + bulb1 = Bulb.create! name: "defaulty", car: car + bulb2 = Bulb.create! name: "other", car: car + + assert_equal [bulb1], car.bulbs + assert_equal [bulb2], car.other_bulbs + end + + test "can rewhere the default scope of the associated model" do + Car.has_many :old_bulbs, -> { rewhere(name: 'old') }, class_name: "Bulb" + car = Car.create! + bulb1 = Bulb.create! name: "defaulty", car: car + bulb2 = Bulb.create! name: "old", car: car + + assert_equal [bulb1], car.bulbs + assert_equal [bulb2], car.old_bulbs + end + test 'unscopes the default scope of associated model when used with include' do car = Car.create! bulb = Bulb.create! name: "other", car: car @@ -2129,11 +2236,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase car = Car.create! original_child = FailedBulb.create!(car: car) - assert_raise(ActiveRecord::RecordNotDestroyed) do + error = assert_raise(ActiveRecord::RecordNotDestroyed) do car.failed_bulbs = [FailedBulb.create!] end assert_equal [original_child], car.reload.failed_bulbs + assert_equal "Failed to destroy the record", error.message end test 'updates counter cache when default scope is given' do @@ -2173,7 +2281,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [], authors(:david).posts_with_signature.map(&:title) end - test 'associations autosaves when object is already persited' do + test 'associations autosaves when object is already persisted' do bulb = Bulb.create! tyre = Tyre.create! @@ -2249,4 +2357,54 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [first_bulb, second_bulb], car.bulbs end + + test 'double insertion of new object to association when same association used in the after create callback of a new object' do + car = Car.create! + car.bulbs << TrickyBulb.new + assert_equal 1, car.bulbs.size + end + + def test_association_force_reload_with_only_true_is_deprecated + company = Company.find(1) + + assert_deprecated { company.clients_of_firm(true) } + end + + class AuthorWithErrorDestroyingAssociation < ActiveRecord::Base + self.table_name = "authors" + has_many :posts_with_error_destroying, + class_name: "PostWithErrorDestroying", + foreign_key: :author_id, + dependent: :destroy + end + + class PostWithErrorDestroying < ActiveRecord::Base + self.table_name = "posts" + self.inheritance_column = nil + before_destroy -> { throw :abort } + end + + def test_destroy_does_not_raise_when_association_errors_on_destroy + assert_no_difference "AuthorWithErrorDestroyingAssociation.count" do + author = AuthorWithErrorDestroyingAssociation.first + + assert_not author.destroy + end + end + + def test_destroy_with_bang_bubbles_errors_from_associations + error = assert_raises ActiveRecord::RecordNotDestroyed do + AuthorWithErrorDestroyingAssociation.first.destroy! + end + + assert_instance_of PostWithErrorDestroying, error.record + end + + def test_ids_reader_memoization + car = Car.create!(name: 'Tofaş') + bulb = Bulb.create!(car: car) + + assert_equal [bulb.id], car.bulb_ids + assert_no_queries { car.bulb_ids } + end end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 5f52c65412..aa35844a03 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -11,7 +11,9 @@ require 'models/tagging' require 'models/author' require 'models/owner' require 'models/pet' +require 'models/pet_treasure' require 'models/toy' +require 'models/treasure' require 'models/contract' require 'models/company' require 'models/developer' @@ -63,7 +65,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } end - def test_ordered_habtm + def test_ordered_has_many_through person_prime = Class.new(ActiveRecord::Base) do def self.name; 'Person'; end @@ -84,11 +86,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase subscriber = make_model "Subscriber" subscriber.primary_key = 'nick' - subscription.belongs_to :book, class: book - subscription.belongs_to :subscriber, class: subscriber + subscription.belongs_to :book, anonymous_class: book + subscription.belongs_to :subscriber, anonymous_class: subscriber - book.has_many :subscriptions, class: subscription - book.has_many :subscribers, through: :subscriptions, class: subscriber + book.has_many :subscriptions, anonymous_class: subscription + book.has_many :subscribers, through: :subscriptions, anonymous_class: subscriber anonbook = book.first namebook = Book.find anonbook.id @@ -154,10 +156,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase lesson_student = make_model 'LessonStudent' lesson_student.table_name = 'lessons_students' - lesson_student.belongs_to :lesson, :class => lesson - lesson_student.belongs_to :student, :class => student - lesson.has_many :lesson_students, :class => lesson_student - lesson.has_many :students, :through => :lesson_students, :class => student + lesson_student.belongs_to :lesson, :anonymous_class => lesson + lesson_student.belongs_to :student, :anonymous_class => student + lesson.has_many :lesson_students, :anonymous_class => lesson_student + lesson.has_many :students, :through => :lesson_students, :anonymous_class => student [lesson, lesson_student, student] end @@ -188,7 +190,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert post.people.include?(person) end - assert post.reload.people(true).include?(person) + assert post.reload.people.reload.include?(person) end def test_delete_all_for_with_dependent_option_destroy @@ -229,7 +231,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post = posts(:thinking) post.people.concat [person] assert_equal 1, post.people.size - assert_equal 1, post.people(true).size + assert_equal 1, post.people.reload.size end def test_associate_existing_record_twice_should_add_to_target_twice @@ -285,7 +287,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert posts(:thinking).people.include?(new_person) end - assert posts(:thinking).reload.people(true).include?(new_person) + assert posts(:thinking).reload.people.reload.include?(new_person) end def test_associate_new_by_building @@ -310,8 +312,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase posts(:thinking).save end - assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Bob") - assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Ted") + assert posts(:thinking).reload.people.reload.collect(&:first_name).include?("Bob") + assert posts(:thinking).reload.people.reload.collect(&:first_name).include?("Ted") end def test_build_then_save_with_has_many_inverse @@ -356,7 +358,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert posts(:welcome).people.empty? end - assert posts(:welcome).reload.people(true).empty? + assert posts(:welcome).reload.people.reload.empty? end def test_destroy_association @@ -367,7 +369,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end assert posts(:welcome).reload.people.empty? - assert posts(:welcome).people(true).empty? + assert posts(:welcome).people.reload.empty? end def test_destroy_all @@ -378,7 +380,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end assert posts(:welcome).reload.people.empty? - assert posts(:welcome).people(true).empty? + assert posts(:welcome).people.reload.empty? end def test_should_raise_exception_for_destroying_mismatching_records @@ -539,7 +541,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_replace_association - assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)} + assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people.reload} # 1 query to delete the existing reader (michael) # 1 query to associate the new reader (david) @@ -552,8 +554,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert !posts(:welcome).people.include?(people(:michael)) } - assert posts(:welcome).reload.people(true).include?(people(:david)) - assert !posts(:welcome).reload.people(true).include?(people(:michael)) + assert posts(:welcome).reload.people.reload.include?(people(:david)) + assert !posts(:welcome).reload.people.reload.include?(people(:michael)) end def test_replace_order_is_preserved @@ -592,7 +594,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert posts(:thinking).people.collect(&:first_name).include?("Jeb") end - assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Jeb") + assert posts(:thinking).reload.people.reload.collect(&:first_name).include?("Jeb") end def test_through_record_is_built_when_created_with_where @@ -668,7 +670,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_clear_associations - assert_queries(2) { posts(:welcome);posts(:welcome).people(true) } + assert_queries(2) { posts(:welcome);posts(:welcome).people.reload } assert_queries(1) do posts(:welcome).people.clear @@ -678,7 +680,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert posts(:welcome).people.empty? end - assert posts(:welcome).reload.people(true).empty? + assert posts(:welcome).reload.people.reload.empty? end def test_association_callback_ordering @@ -744,13 +746,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_has_many_through_with_conditions_should_not_preload Tagging.create!(:taggable_type => 'Post', :taggable_id => posts(:welcome).id, :tag => tags(:misc)) - ActiveRecord::Associations::Preloader.expects(:new).never - posts(:welcome).misc_tag_ids + assert_not_called(ActiveRecord::Associations::Preloader, :new) do + posts(:welcome).misc_tag_ids + end end def test_get_ids_for_loaded_associations person = people(:michael) - person.posts(true) + person.posts.reload assert_queries(0) do person.post_ids person.post_ids @@ -765,9 +768,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_association_proxy_transaction_method_starts_transaction_in_association_class - Tag.expects(:transaction) - Post.first.tags.transaction do - # nothing + assert_called(Tag, :transaction) do + Post.first.tags.transaction do + # nothing + end end end @@ -828,14 +832,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase category = author.named_categories.build(:name => "Primary") author.save assert Categorization.exists?(:author_id => author.id, :named_category_name => category.name) - assert author.named_categories(true).include?(category) + assert author.named_categories.reload.include?(category) end def test_collection_create_with_nonstandard_primary_key_on_belongs_to author = authors(:mary) category = author.named_categories.create(:name => "Primary") assert Categorization.exists?(:author_id => author.id, :named_category_name => category.name) - assert author.named_categories(true).include?(category) + assert author.named_categories.reload.include?(category) end def test_collection_exists @@ -850,7 +854,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase category = author.named_categories.create(:name => "Primary") author.named_categories.delete(category) assert !Categorization.exists?(:author_id => author.id, :named_category_name => category.name) - assert author.named_categories(true).empty? + assert author.named_categories.reload.empty? end def test_collection_singular_ids_getter_with_string_primary_keys @@ -871,10 +875,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_nothing_raised do book = books(:awdr) book.subscriber_ids = [subscribers(:second).nick] - assert_equal [subscribers(:second)], book.subscribers(true) + assert_equal [subscribers(:second)], book.subscribers.reload book.subscriber_ids = [] - assert_equal [], book.subscribers(true) + assert_equal [], book.subscribers.reload end end @@ -882,7 +886,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set company = companies(:rails_core) ids = [Developer.first.id, -9999] - assert_raises(ActiveRecord::RecordNotFound) {company.developer_ids= ids} + assert_raises(ActiveRecord::AssociationTypeMismatch) {company.developer_ids= ids} end def test_build_a_model_from_hm_through_association_with_where_clause @@ -960,7 +964,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal 1, category.categorizations.where(:special => true).count end - def test_joining_has_many_through_with_uniq + def test_joining_has_many_through_with_distinct mary = Author.joins(:unique_categorized_posts).where(:id => authors(:mary).id).first assert_equal 1, mary.unique_categorized_posts.length assert_equal 1, mary.unique_categorized_post_ids.length @@ -1040,14 +1044,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - def test_save_should_not_raise_exception_when_join_record_has_errors - repair_validations(Categorization) do - Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' } - c = Category.create(:name => 'Fishing', :authors => [Author.first]) - c.save - end - end - def test_assign_array_to_new_record_builds_join_records c = Category.new(:name => 'Fishing', :authors => [Author.first]) assert_equal 1, c.categorizations.size @@ -1072,11 +1068,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - def test_create_bang_returns_falsy_when_join_record_has_errors + def test_save_returns_falsy_when_join_record_has_errors repair_validations(Categorization) do Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' } c = Category.new(:name => 'Fishing', :authors => [Author.first]) - assert !c.save + assert_not c.save end end @@ -1088,6 +1084,18 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal [], person.posts end + def test_preloading_empty_through_with_polymorphic_source_association + owner = Owner.create!(name: "Rainbow Unicat") + pet = Pet.create!(owner: owner) + person = Person.create!(first_name: "Gaga") + treasure = Treasure.create!(looter: person) + non_looted_treasure = Treasure.create!() + PetTreasure.create!(pet: pet, treasure: treasure, rainbow_color: "Ultra violet indigo") + PetTreasure.create!(pet: pet, treasure: non_looted_treasure, rainbow_color: "Ultra violet indigo") + + assert_equal [person], Owner.where(name: "Rainbow Unicat").includes(pets: :persons).first.persons.to_a + end + def test_explicitly_joining_join_table assert_equal owners(:blackbeard).toys, owners(:blackbeard).toys.with_pet end @@ -1117,10 +1125,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_through_with_default_scope_on_the_target person = people(:michael) - assert_equal [posts(:thinking)], person.first_posts + assert_equal [posts(:thinking).id], person.first_posts.map(&:id) readers(:michael_authorless).update(first_post_id: 1) - assert_equal [posts(:thinking)], person.reload.first_posts + assert_equal [posts(:thinking).id], person.reload.first_posts.map(&:id) end def test_has_many_through_with_includes_in_through_association_scope @@ -1166,4 +1174,45 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post_through = organization.posts.build assert_equal post_direct.author_id, post_through.author_id end + + def test_has_many_through_with_scope_that_should_not_be_fully_merged + Club.has_many :distinct_memberships, -> { distinct }, class_name: "Membership" + Club.has_many :special_favourites, through: :distinct_memberships, source: :member + + assert_nil Club.new.special_favourites.distinct_value + end + + def test_association_force_reload_with_only_true_is_deprecated + post = Post.find(1) + + assert_deprecated { post.people(true) } + end + + def test_has_many_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes + member = Member.create! + club = Club.create! + TenantMembership.create!( + member: member, + club: club + ) + + TenantMembership.current_member = member + + tenant_clubs = member.tenant_clubs + assert_equal [club], tenant_clubs + + TenantMembership.current_member = nil + + other_member = Member.create! + other_club = Club.create! + TenantMembership.create!( + member: other_member, + club: other_club + ) + + tenant_clubs = other_member.tenant_clubs + assert_equal [other_club], tenant_clubs + ensure + TenantMembership.current_member = nil + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 5c2e5e7b43..c9d9e29f09 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -107,6 +107,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_nil Account.find(old_account_id).firm_id end + def test_nullification_on_destroyed_association + developer = Developer.create!(name: "Someone") + ship = Ship.create!(name: "Planet Caravan", developer: developer) + ship.destroy + assert !ship.persisted? + assert !developer.persisted? + end + def test_natural_assignment_to_nil_after_destroy firm = companies(:rails_core) old_account_id = firm.account.id @@ -178,6 +186,25 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert firm.account.present? end + def test_restrict_with_error_is_deprecated_using_key_one + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations :en, activerecord: { errors: { messages: { restrict_dependent_destroy: { one: 'message for deprecated key' } } } } + + firm = RestrictedWithErrorFirm.create!(name: 'restrict') + firm.create_account(credit_limit: 10) + + assert_not_nil firm.account + + assert_deprecated { firm.destroy } + + assert !firm.errors.empty? + assert_equal 'message for deprecated key', firm.errors[:base].first + assert RestrictedWithErrorFirm.exists?(name: 'restrict') + assert firm.account.present? + ensure + I18n.backend.reload! + end + def test_restrict_with_error firm = RestrictedWithErrorFirm.create!(:name => 'restrict') firm.create_account(:credit_limit => 10) @@ -192,6 +219,24 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert firm.account.present? end + def test_restrict_with_error_with_locale + I18n.backend = I18n::Backend::Simple.new + I18n.backend.store_translations 'en', activerecord: {attributes: {restricted_with_error_firm: {account: 'firm account'}}} + firm = RestrictedWithErrorFirm.create!(name: 'restrict') + firm.create_account(credit_limit: 10) + + assert_not_nil firm.account + + firm.destroy + + assert !firm.errors.empty? + assert_equal "Cannot delete record because a dependent firm account exists", firm.errors[:base].first + assert RestrictedWithErrorFirm.exists?(name: 'restrict') + assert firm.account.present? + ensure + I18n.backend.reload! + end + def test_successful_build_association firm = Firm.new("name" => "GlobalMegaCorp") firm.save @@ -332,7 +377,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert a.persisted? assert_equal a, firm.account assert_equal a, firm.account - assert_equal a, firm.account(true) + firm.association(:account).reload + assert_equal a, firm.account end def test_save_still_works_after_accessing_nil_has_one @@ -607,4 +653,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end end end + + def test_association_force_reload_with_only_true_is_deprecated + firm = Firm.find(1) + + assert_deprecated { firm.account(true) } + end end diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index f8772547a2..b2b46812b9 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -16,6 +16,10 @@ require 'models/owner' require 'models/post' require 'models/comment' require 'models/categorization' +require 'models/customer' +require 'models/carrier' +require 'models/shop_account' +require 'models/customer_carrier' class HasOneThroughAssociationsTest < ActiveRecord::TestCase fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans, @@ -245,12 +249,14 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_not_nil @member_detail.member_type @member_detail.destroy assert_queries(1) do - assert_not_nil @member_detail.member_type(true) + @member_detail.association(:member_type).reload + assert_not_nil @member_detail.member_type end @member_detail.member.destroy assert_queries(1) do - assert_nil @member_detail.member_type(true) + @member_detail.association(:member_type).reload + assert_nil @member_detail.member_type end end @@ -344,4 +350,34 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end end end + + def test_has_one_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes + customer = Customer.create! + carrier = Carrier.create! + customer_carrier = CustomerCarrier.create!( + customer: customer, + carrier: carrier, + ) + account = ShopAccount.create!(customer_carrier: customer_carrier) + + CustomerCarrier.current_customer = customer + + account_carrier = account.carrier + assert_equal carrier, account_carrier + + CustomerCarrier.current_customer = nil + + other_carrier = Carrier.create! + other_customer = Customer.create! + other_customer_carrier = CustomerCarrier.create!( + customer: other_customer, + carrier: other_carrier, + ) + other_account = ShopAccount.create!(customer_carrier: other_customer_carrier) + + account_carrier = other_account.carrier + assert_equal other_carrier, account_carrier + ensure + CustomerCarrier.current_customer = nil + end end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 423b8238b1..c9743e80d3 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -13,6 +13,9 @@ require 'models/mixed_case_monkey' require 'models/admin' require 'models/admin/account' require 'models/admin/user' +require 'models/developer' +require 'models/company' +require 'models/project' class AutomaticInverseFindingTests < ActiveRecord::TestCase fixtures :ratings, :comments, :cars @@ -80,10 +83,10 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase assert_equal rating.comment, comment, "The Rating's comment should be the original Comment" - rating.comment.body = "Brogramming is the act of programming, like a bro." + rating.comment.body = "Fennec foxes are the smallest of the foxes." assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body" - comment.body = "Broseiden is the king of the sea of bros." + comment.body = "Kittens are adorable." assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association" end @@ -94,10 +97,10 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase assert_equal rating.comment, comment, "The Rating's comment should be the original Comment" - rating.comment.body = "Brogramming is the act of programming, like a bro." + rating.comment.body = "Fennec foxes are the smallest of the foxes." assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body" - comment.body = "Broseiden is the king of the sea of bros." + comment.body = "Kittens are adorable." assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association" end @@ -127,15 +130,15 @@ end class InverseAssociationTests < ActiveRecord::TestCase def test_should_allow_for_inverse_of_options_in_associations - assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on has_many') do + assert_nothing_raised do Class.new(ActiveRecord::Base).has_many(:wheels, :inverse_of => :car) end - assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on has_one') do + assert_nothing_raised do Class.new(ActiveRecord::Base).has_one(:engine, :inverse_of => :car) end - assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on belongs_to') do + assert_nothing_raised do Class.new(ActiveRecord::Base).belongs_to(:car, :inverse_of => :driver) end end @@ -198,6 +201,16 @@ class InverseAssociationTests < ActiveRecord::TestCase belongs_to_ref = Sponsor.reflect_on_association(:sponsor_club) assert_nil belongs_to_ref.inverse_of end + + def test_this_inverse_stuff + firm = Firm.create!(name: 'Adequate Holdings') + Project.create!(name: 'Project 1', firm: firm) + Developer.create!(name: 'Gorbypuff', firm: firm) + + new_project = Project.last + assert Project.reflect_on_association(:lead_developer).inverse_of.present?, "Expected inverse of to be present" + assert new_project.lead_developer.present?, "Expected lead developer to be present on the project" + end end class InverseHasOneTests < ActiveRecord::TestCase @@ -653,7 +666,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase def test_trying_to_access_inverses_that_dont_exist_shouldnt_raise_an_error # Ideally this would, if only for symmetry's sake with other association types - assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_polymorphic_man } + assert_nothing_raised { Face.first.horrible_polymorphic_man } end def test_trying_to_set_polymorphic_inverses_that_dont_exist_at_all_should_raise_an_error @@ -663,7 +676,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase def test_trying_to_set_polymorphic_inverses_that_dont_exist_on_the_instance_being_set_should_raise_an_error # passes because Man does have the correct inverse_of - assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Man.first } + assert_nothing_raised { Face.first.polymorphic_man = Man.first } # fails because Interest does have the correct inverse_of assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first } end @@ -675,7 +688,7 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase fixtures :men, :interests, :zines def test_that_we_can_load_associations_that_have_the_same_reciprocal_name_from_different_models - assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do + assert_nothing_raised do i = Interest.first i.zine i.man @@ -683,7 +696,7 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase end def test_that_we_can_create_associations_that_have_the_same_reciprocal_name_from_different_models - assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do + assert_nothing_raised do i = Interest.first i.build_zine(:title => 'Get Some in Winter! 2008') i.build_man(:name => 'Gordon') diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 213be50e67..c7bd9d2119 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -35,12 +35,12 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert categories(:sti_test).authors.include?(authors(:mary)) end - def test_has_many_uniq_through_join_model + def test_has_many_distinct_through_join_model assert_equal 2, authors(:mary).categorized_posts.size assert_equal 1, authors(:mary).unique_categorized_posts.size end - def test_has_many_uniq_through_count + def test_has_many_distinct_through_count author = authors(:mary) assert !authors(:mary).unique_categorized_posts.loaded? assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count } @@ -49,7 +49,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert !authors(:mary).unique_categorized_posts.loaded? end - def test_has_many_uniq_through_find + def test_has_many_distinct_through_find assert_equal 1, authors(:mary).unique_categorized_posts.to_a.size end @@ -88,7 +88,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_polymorphic_has_many_going_through_join_model_with_custom_select_and_joins assert_equal tags(:general), tag = posts(:welcome).tags.add_joins_and_select.first - assert_nothing_raised(NoMethodError) { tag.author_id } + assert_nothing_raised { tag.author_id } end def test_polymorphic_has_many_going_through_join_model_with_custom_foreign_key @@ -213,7 +213,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase old_count = Tagging.count post.destroy assert_equal old_count-1, Tagging.count - assert_nil posts(:welcome).tagging(true) + posts(:welcome).association(:tagging).reload + assert_nil posts(:welcome).tagging end def test_delete_polymorphic_has_one_with_nullify @@ -224,7 +225,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase old_count = Tagging.count post.destroy assert_equal old_count, Tagging.count - assert_nil posts(:welcome).tagging(true) + posts(:welcome).association(:tagging).reload + assert_nil posts(:welcome).tagging end def test_has_many_with_piggyback @@ -361,6 +363,13 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_equal posts(:welcome, :thinking).sort_by(&:id), tags(:general).tagged_posts.sort_by(&:id) end + def test_has_many_polymorphic_associations_merges_through_scope + Tag.has_many :null_taggings, -> { none }, class_name: :Tagging + Tag.has_many :null_tagged_posts, :through => :null_taggings, :source => 'taggable', :source_type => 'Post' + assert_equal [], tags(:general).null_tagged_posts + refute_equal [], tags(:general).tagged_posts + end + def test_eager_has_many_polymorphic_with_source_type tag_with_include = Tag.all.merge!(:includes => :tagged_posts).find(tags(:general).id) desired = posts(:welcome, :thinking) @@ -461,7 +470,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert saved_post.tags.include?(new_tag) assert new_tag.persisted? - assert saved_post.reload.tags(true).include?(new_tag) + assert saved_post.reload.tags.reload.include?(new_tag) new_post = Post.new(:title => "Association replacement works!", :body => "You best believe it.") @@ -474,7 +483,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase new_post.save! assert new_post.persisted? - assert new_post.reload.tags(true).include?(saved_tag) + assert new_post.reload.tags.reload.include?(saved_tag) assert !posts(:thinking).tags.build.persisted? assert !posts(:thinking).tags.new.persisted? @@ -490,7 +499,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") assert_equal(count + 1, post_thinking.reload.tags.size) - assert_equal(count + 1, post_thinking.tags(true).size) + assert_equal(count + 1, post_thinking.tags.reload.size) assert_kind_of Tag, post_thinking.tags.create!(:name => 'foo') assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag }, @@ -498,7 +507,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") assert_equal(count + 2, post_thinking.reload.tags.size) - assert_equal(count + 2, post_thinking.tags(true).size) + assert_equal(count + 2, post_thinking.tags.reload.size) assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) } assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag }, @@ -506,7 +515,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") assert_equal(count + 4, post_thinking.reload.tags.size) - assert_equal(count + 4, post_thinking.tags(true).size) + assert_equal(count + 4, post_thinking.tags.reload.size) # Raises if the wrong reflection name is used to set the Edge belongs_to assert_nothing_raised { vertices(:vertex_1).sinks << vertices(:vertex_5) } @@ -544,11 +553,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase book = Book.create!(:name => 'Getting Real') book_awdr = books(:awdr) book_awdr.references << book - assert_equal(count + 1, book_awdr.references(true).size) + assert_equal(count + 1, book_awdr.references.reload.size) assert_nothing_raised { book_awdr.references.delete(book) } assert_equal(count, book_awdr.references.size) - assert_equal(count, book_awdr.references(true).size) + assert_equal(count, book_awdr.references.reload.size) assert_equal(references_before.sort, book_awdr.references.sort) end @@ -558,14 +567,14 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase tag = Tag.create!(:name => 'doomed') post_thinking = posts(:thinking) post_thinking.tags << tag - assert_equal(count + 1, post_thinking.taggings(true).size) - assert_equal(count + 1, post_thinking.reload.tags(true).size) + assert_equal(count + 1, post_thinking.taggings.reload.size) + assert_equal(count + 1, post_thinking.reload.tags.reload.size) assert_not_equal(tags_before, post_thinking.tags.sort) assert_nothing_raised { post_thinking.tags.delete(tag) } assert_equal(count, post_thinking.tags.size) - assert_equal(count, post_thinking.tags(true).size) - assert_equal(count, post_thinking.taggings(true).size) + assert_equal(count, post_thinking.tags.reload.size) + assert_equal(count, post_thinking.taggings.reload.size) assert_equal(tags_before, post_thinking.tags.sort) end @@ -577,11 +586,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase quaked = Tag.create!(:name => 'quaked') post_thinking = posts(:thinking) post_thinking.tags << doomed << doomed2 - assert_equal(count + 2, post_thinking.reload.tags(true).size) + assert_equal(count + 2, post_thinking.reload.tags.reload.size) assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) } assert_equal(count, post_thinking.tags.size) - assert_equal(count, post_thinking.tags(true).size) + assert_equal(count, post_thinking.tags.reload.size) assert_equal(tags_before, post_thinking.tags.sort) end @@ -625,7 +634,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_equal [comments(:does_it_hurt)], authors(:david).special_post_comments end - def test_uniq_has_many_through_should_retain_order + def test_distinct_has_many_through_should_retain_order comment_ids = authors(:david).comments.map(&:id) assert_equal comment_ids.sort, authors(:david).ordered_uniq_comments.map(&:id) assert_equal comment_ids.sort.reverse, authors(:david).ordered_uniq_comments_desc.map(&:id) @@ -738,6 +747,23 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_equal aircraft.engines, [engine] end + def test_proper_error_message_for_eager_load_and_includes_association_errors + includes_error = assert_raises(ActiveRecord::ConfigurationError) { + Post.includes(:nonexistent_relation).where(nonexistent_relation: {name: 'Rochester'}).find(1) + } + assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", includes_error.message) + + eager_load_error = assert_raises(ActiveRecord::ConfigurationError) { + Post.eager_load(:nonexistent_relation).where(nonexistent_relation: {name: 'Rochester'}).find(1) + } + assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", eager_load_error.message) + + includes_and_eager_load_error = assert_raises(ActiveRecord::ConfigurationError) { + Post.eager_load(:nonexistent_relation).includes(:nonexistent_relation).where(nonexistent_relation: {name: 'Rochester'}).find(1) + } + assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", includes_and_eager_load_error.message) + end + private # create dynamic Post models to allow different dependency options def find_post_with_dependency(post_id, association, association_name, dependency) diff --git a/activerecord/test/cases/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb new file mode 100644 index 0000000000..4af791b758 --- /dev/null +++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb @@ -0,0 +1,79 @@ +require "cases/helper" +require 'models/post' +require 'models/comment' +require 'models/author' +require 'models/essay' +require 'models/categorization' +require 'models/person' + +class LeftOuterJoinAssociationTest < ActiveRecord::TestCase + fixtures :authors, :essays, :posts, :comments, :categorizations, :people + + def test_construct_finder_sql_applies_aliases_tables_on_association_conditions + result = Author.left_outer_joins(:thinking_posts, :welcome_posts).to_a + assert_equal authors(:david), result.first + end + + def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations + assert_nothing_raised do + queries = capture_sql do + Person.left_outer_joins(:agents => {:agents => :agents}) + .left_outer_joins(:agents => {:agents => {:primary_contact => :agents}}).to_a + end + assert queries.any? { |sql| /agents_people_4/i =~ sql } + end + end + + def test_construct_finder_sql_executes_a_left_outer_join + assert_not_equal Author.count, Author.joins(:posts).count + assert_equal Author.count, Author.left_outer_joins(:posts).count + end + + def test_left_outer_join_by_left_joins + assert_not_equal Author.count, Author.joins(:posts).count + assert_equal Author.count, Author.left_joins(:posts).count + end + + def test_construct_finder_sql_ignores_empty_left_outer_joins_hash + queries = capture_sql { Author.left_outer_joins({}) } + assert queries.none? { |sql| /LEFT OUTER JOIN/i =~ sql } + end + + def test_construct_finder_sql_ignores_empty_left_outer_joins_array + queries = capture_sql { Author.left_outer_joins([]) } + assert queries.none? { |sql| /LEFT OUTER JOIN/i =~ sql } + end + + def test_left_outer_joins_forbids_to_use_string_as_argument + assert_raise(ArgumentError){ Author.left_outer_joins('LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"').to_a } + end + + def test_join_conditions_added_to_join_clause + queries = capture_sql { Author.left_outer_joins(:essays).to_a } + assert queries.any? { |sql| /writer_type.*?=.*?(Author|\?|\$1)/i =~ sql } + assert queries.none? { |sql| /WHERE/i =~ sql } + end + + def test_find_with_sti_join + scope = Post.left_outer_joins(:special_comments).where(:id => posts(:sti_comments).id) + + # The join should match SpecialComment and its subclasses only + assert scope.where("comments.type" => "Comment").empty? + assert !scope.where("comments.type" => "SpecialComment").empty? + assert !scope.where("comments.type" => "SubSpecialComment").empty? + end + + def test_does_not_override_select + authors = Author.select("authors.name, #{%{(authors.author_address_id || ' ' || authors.author_address_extra_id) as addr_id}}").left_outer_joins(:posts) + assert authors.any? + assert authors.first.respond_to?(:addr_id) + end + + test "the default scope of the target is applied when joining associations" do + author = Author.create! name: "Jon" + author.categorizations.create! + author.categorizations.create! special: true + + assert_equal [author], Author.where(id: author).left_outer_joins(:special_categorizations) + end +end diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 31b68c940e..b040485d99 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -495,7 +495,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase groucho = members(:groucho) founding = member_types(:founding) - assert_raises(ActiveRecord::HasManyThroughNestedAssociationsAreReadonly) do + assert_raises(ActiveRecord::HasOneThroughNestedAssociationsAreReadonly) do groucho.nested_member_type = founding end end diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 3d202a5527..01a058918a 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -1,7 +1,6 @@ require "cases/helper" require 'models/computer' require 'models/developer' -require 'models/computer' require 'models/project' require 'models/company' require 'models/categorization' @@ -13,7 +12,6 @@ require 'models/tag' require 'models/tagging' require 'models/person' require 'models/reader' -require 'models/parrot' require 'models/ship_part' require 'models/ship' require 'models/liquid' @@ -93,8 +91,10 @@ class AssociationsTest < ActiveRecord::TestCase assert firm.clients.empty?, "New firm should have cached no client objects" assert_equal 0, firm.clients.size, "New firm should have cached 0 clients count" - assert !firm.clients(true).empty?, "New firm should have reloaded client objects" - assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count" + ActiveSupport::Deprecation.silence do + assert !firm.clients(true).empty?, "New firm should have reloaded client objects" + assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count" + end end def test_using_limitable_reflections_helper @@ -110,10 +110,13 @@ class AssociationsTest < ActiveRecord::TestCase def test_force_reload_is_uncached firm = Firm.create!("name" => "A New Firm, Inc") Client.create!("name" => "TheClient.com", :firm => firm) - ActiveRecord::Base.cache do - firm.clients.each {} - assert_queries(0) { assert_not_nil firm.clients.each {} } - assert_queries(1) { assert_not_nil firm.clients(true).each {} } + + ActiveSupport::Deprecation.silence do + ActiveRecord::Base.cache do + firm.clients.each {} + assert_queries(0) { assert_not_nil firm.clients.each {} } + assert_queries(1) { assert_not_nil firm.clients(true).each {} } + end end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index ea2b94cbf4..1db52af59b 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -1,7 +1,6 @@ require "cases/helper" require 'models/minimalistic' require 'models/developer' -require 'models/computer' require 'models/auto_id' require 'models/boolean' require 'models/computer' @@ -67,8 +66,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase def test_caching_nil_primary_key klass = Class.new(Minimalistic) - klass.expects(:reset_primary_key).returns(nil).once - 2.times { klass.primary_key } + assert_called(klass, :reset_primary_key, returns: nil) do + 2.times { klass.primary_key } + end end def test_attribute_keys_on_new_instance @@ -175,9 +175,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal category_attrs , category.attributes_before_type_cast end - if current_adapter?(:MysqlAdapter) + if current_adapter?(:Mysql2Adapter) def test_read_attributes_before_type_cast_on_boolean - bool = Boolean.create({ "value" => false }) + bool = Boolean.create!({ "value" => false }) if RUBY_PLATFORM =~ /java/ # JRuby will return the value before typecast as string assert_equal "0", bool.reload.attributes_before_type_cast["value"] @@ -542,9 +542,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase developer.save! - assert_equal "50000", developer.salary_before_type_cast - assert_equal 1337, developer.name_before_type_cast - assert_equal 50000, developer.salary assert_equal "1337", developer.name end @@ -717,6 +714,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_time_zone_aware_attributes_dont_recurse_infinitely_on_invalid_values + in_time_zone "Pacific Time (US & Canada)" do + record = @target.new(bonus_time: []) + assert_equal nil, record.bonus_time + end + end + def test_setting_time_zone_conversion_for_attributes_should_write_value_on_class_variable Topic.skip_time_zone_conversion_for_attributes = [:field_a] Minimalistic.skip_time_zone_conversion_for_attributes = [:field_b] @@ -794,7 +798,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_nil computer.system end - def test_global_methods_are_overwritte_when_subclassing + def test_global_methods_are_overwritten_when_subclassing klass = Class.new(ActiveRecord::Base) { self.abstract_class = true } subklass = Class.new(klass) do diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb index 9d927481ec..7a24b85a36 100644 --- a/activerecord/test/cases/attribute_set_test.rb +++ b/activerecord/test/cases/attribute_set_test.rb @@ -29,7 +29,7 @@ module ActiveRecord assert_equal :bar, attributes[:bar].name end - test "duping creates a new hash and dups each attribute" do + test "duping creates a new hash, but does not dup the attributes" do builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new) attributes = builder.build_from_database(foo: 1, bar: 'foo') @@ -43,6 +43,24 @@ module ActiveRecord assert_equal 1, attributes[:foo].value assert_equal 2, duped[:foo].value + assert_equal 'foobar', attributes[:bar].value + assert_equal 'foobar', duped[:bar].value + end + + test "deep_duping creates a new hash and dups each attribute" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new) + attributes = builder.build_from_database(foo: 1, bar: 'foo') + + # Ensure the type cast value is cached + attributes[:foo].value + attributes[:bar].value + + duped = attributes.deep_dup + duped.write_from_database(:foo, 2) + duped[:bar].value << 'bar' + + assert_equal 1, attributes[:foo].value + assert_equal 2, duped[:foo].value assert_equal 'foo', attributes[:bar].value assert_equal 'foobar', duped[:bar].value end @@ -160,6 +178,9 @@ module ActiveRecord return if value.nil? value + " from database" end + + def assert_valid_value(*) + end end test "write_from_database sets the attribute with database typecasting" do @@ -207,5 +228,26 @@ module ActiveRecord assert_equal [:foo], attributes.accessed end + + test "#map returns a new attribute set with the changes applied" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) + attributes = builder.build_from_database(foo: "1", bar: "2") + new_attributes = attributes.map do |attr| + attr.with_cast_value(attr.value + 1) + end + + assert_equal 2, new_attributes.fetch_value(:foo) + assert_equal 3, new_attributes.fetch_value(:bar) + end + + test "comparison for equality is correctly implemented" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) + attributes = builder.build_from_database(foo: "1", bar: "2") + attributes2 = builder.build_from_database(foo: "1", bar: "2") + attributes3 = builder.build_from_database(foo: "2", bar: "2") + + assert_equal attributes, attributes2 + assert_not_equal attributes2, attributes3 + end end end diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb index aa419c7a67..a24a4fc6a4 100644 --- a/activerecord/test/cases/attribute_test.rb +++ b/activerecord/test/cases/attribute_test.rb @@ -1,11 +1,9 @@ require 'cases/helper' -require 'minitest/mock' module ActiveRecord class AttributeTest < ActiveRecord::TestCase setup do @type = Minitest::Mock.new - @type.expect(:==, false, [false]) end teardown do @@ -109,6 +107,9 @@ module ActiveRecord def deserialize(value) value + " from database" end + + def assert_valid_value(*) + end end test "with_value_from_user returns a new attribute with the value from the user" do @@ -181,12 +182,65 @@ module ActiveRecord assert attribute.has_been_read? end + test "an attribute is not changed if it hasn't been assigned or mutated" do + attribute = Attribute.from_database(:foo, 1, Type::Value.new) + + refute attribute.changed? + end + + test "an attribute is changed if it's been assigned a new value" do + attribute = Attribute.from_database(:foo, 1, Type::Value.new) + changed = attribute.with_value_from_user(2) + + assert changed.changed? + end + + test "an attribute is not changed if it's assigned the same value" do + attribute = Attribute.from_database(:foo, 1, Type::Value.new) + unchanged = attribute.with_value_from_user(1) + + refute unchanged.changed? + end + test "an attribute can not be mutated if it has not been read, and skips expensive calculations" do type_which_raises_from_all_methods = Object.new attribute = Attribute.from_database(:foo, "bar", type_which_raises_from_all_methods) - assert_not attribute.changed_in_place_from?("bar") + assert_not attribute.changed_in_place? + end + + test "an attribute is changed if it has been mutated" do + attribute = Attribute.from_database(:foo, "bar", Type::String.new) + attribute.value << "!" + + assert attribute.changed_in_place? + assert attribute.changed? + end + + test "an attribute can forget its changes" do + attribute = Attribute.from_database(:foo, "bar", Type::String.new) + changed = attribute.with_value_from_user("foo") + forgotten = changed.forgetting_assignment + + assert changed.changed? # sanity check + refute forgotten.changed? + end + + test "with_value_from_user validates the value" do + type = Type::Value.new + type.define_singleton_method(:assert_valid_value) do |value| + if value == 1 + raise ArgumentError + end + end + + attribute = Attribute.from_database(:foo, 1, type) + assert_equal 1, attribute.value + assert_equal 2, attribute.with_value_from_user(2).value + assert_raises ArgumentError do + attribute.with_value_from_user(1) + end end end end diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index 927d7950a5..48ba7a63d5 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -63,6 +63,15 @@ module ActiveRecord end end + test "model with nonexistent attribute with default value can be saved" do + klass = Class.new(OverloadedType) do + attribute :non_existent_string_with_default, :string, default: 'nonexistent' + end + + model = klass.new + assert model.save + end + test "changing defaults" do data = OverloadedType.new unoverloaded_data = UnoverloadedType.new @@ -125,8 +134,35 @@ module ActiveRecord assert_equal "from user", model.wibble end + test "procs for default values" do + klass = Class.new(OverloadedType) do + @@counter = 0 + attribute :counter, :integer, default: -> { @@counter += 1 } + end + + assert_equal 1, klass.new.counter + assert_equal 2, klass.new.counter + end + + test "procs are memoized before type casting" do + klass = Class.new(OverloadedType) do + @@counter = 0 + attribute :counter, :integer, default: -> { @@counter += 1 } + end + + model = klass.new + assert_equal 1, model.counter_before_type_cast + assert_equal 1, model.counter_before_type_cast + end + + test "user provided defaults are persisted even if unchanged" do + model = OverloadedType.create! + + assert_equal "the overloaded default", model.reload.string_with_default + end + if current_adapter?(:PostgreSQLAdapter) - test "arrays types can be specified" do + test "array types can be specified" do klass = Class.new(OverloadedType) do attribute :my_array, :string, limit: 50, array: true attribute :my_int_array, :integer, array: true @@ -136,7 +172,7 @@ module ActiveRecord Type::String.new(limit: 50)) int_array = ConnectionAdapters::PostgreSQL::OID::Array.new( Type::Integer.new) - refute_equal string_array, int_array + assert_not_equal string_array, int_array assert_equal string_array, klass.type_for_attribute("my_array") assert_equal int_array, klass.type_for_attribute("my_int_array") end @@ -151,10 +187,23 @@ module ActiveRecord Type::String.new(limit: 50)) int_range = ConnectionAdapters::PostgreSQL::OID::Range.new( Type::Integer.new) - refute_equal string_range, int_range + assert_not_equal string_range, int_range assert_equal string_range, klass.type_for_attribute("my_range") assert_equal int_range, klass.type_for_attribute("my_int_range") end end + + test "attributes added after subclasses load are inherited" do + parent = Class.new(ActiveRecord::Base) do + self.table_name = "topics" + end + + child = Class.new(parent) + child.new # => force a schema load + + parent.attribute(:foo, Type::Value.new) + + assert_equal(:bar, child.new(foo: :bar).foo) + end end end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 8f0d7bd037..9e3266b7d6 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -24,6 +24,8 @@ require 'models/molecule' require 'models/member' require 'models/member_detail' require 'models/organization' +require 'models/guitar' +require 'models/tuning_peg' class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase def test_autosave_validation @@ -43,7 +45,7 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase reference = Class.new(ActiveRecord::Base) { self.table_name = "references" def self.name; 'Reference'; end - belongs_to :person, autosave: true, class: person + belongs_to :person, autosave: true, anonymous_class: person } u = person.create!(first_name: 'cool') @@ -67,6 +69,14 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase assert_no_difference_when_adding_callbacks_twice_for Pirate, :parrots end + def test_cyclic_autosaves_do_not_add_multiple_validations + ship = ShipWithoutNestedAttributes.new + ship.prisoners.build + + assert_not ship.valid? + assert_equal 1, ship.errors[:name].length + end + private def assert_no_difference_when_adding_callbacks_twice_for(model, association_name) @@ -149,7 +159,8 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas assert_equal a, firm.account assert firm.save assert_equal a, firm.account - assert_equal a, firm.account(true) + firm.association(:account).reload + assert_equal a, firm.account end def test_assignment_before_either_saved @@ -162,7 +173,8 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas assert firm.persisted? assert a.persisted? assert_equal a, firm.account - assert_equal a, firm.account(true) + firm.association(:account).reload + assert_equal a, firm.account end def test_not_resaved_when_unchanged @@ -248,7 +260,8 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test assert apple.save assert apple.persisted? assert_equal apple, client.firm - assert_equal apple, client.firm(true) + client.association(:firm).reload + assert_equal apple, client.firm end def test_assignment_before_either_saved @@ -261,7 +274,8 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test assert final_cut.persisted? assert apple.persisted? assert_equal apple, final_cut.firm - assert_equal apple, final_cut.firm(true) + final_cut.association(:firm).reload + assert_equal apple, final_cut.firm end def test_store_two_association_with_one_save @@ -385,6 +399,87 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib assert_not molecule.persisted?, 'Molecule should not be persisted when its electrons are invalid' end + def test_errors_should_be_indexed_when_passed_as_array + guitar = Guitar.new + tuning_peg_valid = TuningPeg.new + tuning_peg_valid.pitch = 440.0 + tuning_peg_invalid = TuningPeg.new + + guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid] + + assert_not tuning_peg_invalid.valid? + assert tuning_peg_valid.valid? + assert_not guitar.valid? + assert_equal ["is not a number"], guitar.errors["tuning_pegs[1].pitch"] + assert_not_equal ["is not a number"], guitar.errors["tuning_pegs.pitch"] + end + + def test_errors_should_be_indexed_when_global_flag_is_set + old_attribute_config = ActiveRecord::Base.index_nested_attribute_errors + ActiveRecord::Base.index_nested_attribute_errors = true + + molecule = Molecule.new + valid_electron = Electron.new(name: 'electron') + invalid_electron = Electron.new + + molecule.electrons = [valid_electron, invalid_electron] + + assert_not invalid_electron.valid? + assert valid_electron.valid? + assert_not molecule.valid? + assert_equal ["can't be blank"], molecule.errors["electrons[1].name"] + assert_not_equal ["can't be blank"], molecule.errors["electrons.name"] + ensure + ActiveRecord::Base.index_nested_attribute_errors = old_attribute_config + end + + def test_errors_details_should_be_set + molecule = Molecule.new + valid_electron = Electron.new(name: 'electron') + invalid_electron = Electron.new + + molecule.electrons = [valid_electron, invalid_electron] + + assert_not invalid_electron.valid? + assert valid_electron.valid? + assert_not molecule.valid? + assert_equal [{error: :blank}], molecule.errors.details["electrons.name"] + end + + def test_errors_details_should_be_indexed_when_passed_as_array + guitar = Guitar.new + tuning_peg_valid = TuningPeg.new + tuning_peg_valid.pitch = 440.0 + tuning_peg_invalid = TuningPeg.new + + guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid] + + assert_not tuning_peg_invalid.valid? + assert tuning_peg_valid.valid? + assert_not guitar.valid? + assert_equal [{error: :not_a_number, value: nil}] , guitar.errors.details["tuning_pegs[1].pitch"] + assert_equal [], guitar.errors.details["tuning_pegs.pitch"] + end + + def test_errors_details_should_be_indexed_when_global_flag_is_set + old_attribute_config = ActiveRecord::Base.index_nested_attribute_errors + ActiveRecord::Base.index_nested_attribute_errors = true + + molecule = Molecule.new + valid_electron = Electron.new(name: 'electron') + invalid_electron = Electron.new + + molecule.electrons = [valid_electron, invalid_electron] + + assert_not invalid_electron.valid? + assert valid_electron.valid? + assert_not molecule.valid? + assert_equal [{error: :blank}], molecule.errors.details["electrons[1].name"] + assert_equal [], molecule.errors.details["electrons.name"] + ensure + ActiveRecord::Base.index_nested_attribute_errors = old_attribute_config + end + def test_valid_adding_with_nested_attributes molecule = Molecule.new valid_electron = Electron.new(name: 'electron') @@ -456,7 +551,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_equal new_client, companies(:first_firm).clients_of_firm.last assert !companies(:first_firm).save assert !new_client.persisted? - assert_equal 2, companies(:first_firm).clients_of_firm(true).size + assert_equal 2, companies(:first_firm).clients_of_firm.reload.size end def test_adding_before_save @@ -481,7 +576,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_equal no_of_clients + 2, Client.count # Clients were saved to database. assert_equal 2, new_firm.clients_of_firm.size - assert_equal 2, new_firm.clients_of_firm(true).size + assert_equal 2, new_firm.clients_of_firm.reload.size end def test_assign_ids @@ -510,7 +605,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(2) { assert company.save } assert new_client.persisted? - assert_equal 3, company.clients_of_firm(true).size + assert_equal 3, company.clients_of_firm.reload.size end def test_build_many_before_save @@ -519,7 +614,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(3) { assert company.save } - assert_equal 4, company.clients_of_firm(true).size + assert_equal 4, company.clients_of_firm.reload.size end def test_build_via_block_before_save @@ -530,7 +625,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(2) { assert company.save } assert new_client.persisted? - assert_equal 3, company.clients_of_firm(true).size + assert_equal 3, company.clients_of_firm.reload.size end def test_build_many_via_block_before_save @@ -543,7 +638,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(3) { assert company.save } - assert_equal 4, company.clients_of_firm(true).size + assert_equal 4, company.clients_of_firm.reload.size end def test_replace_on_new_object @@ -654,7 +749,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end # has_one - def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal + def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction assert !@pirate.ship.marked_for_destruction? @pirate.ship.mark_for_destruction @@ -714,7 +809,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end # belongs_to - def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destroyal + def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction assert !@ship.pirate.marked_for_destruction? @ship.pirate.mark_for_destruction @@ -1142,6 +1237,13 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase def test_should_not_load_the_associated_model assert_queries(1) { @pirate.catchphrase = 'Arr'; @pirate.save! } end + + def test_mark_for_destruction_is_ignored_without_autosave_true + ship = ShipWithoutNestedAttributes.new(name: "The Black Flag") + ship.parts.build.mark_for_destruction + + assert_not ship.valid? + end end class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCase @@ -1278,6 +1380,16 @@ module AutosaveAssociationOnACollectionAssociationTests assert_equal new_names, @pirate.reload.send(@association_name).map(&:name) end + def test_should_update_children_when_autosave_is_true_and_parent_is_new_but_child_is_not + parrot = Parrot.create!(name: "Polly") + parrot.name = "Squawky" + pirate = Pirate.new(parrots: [parrot], catchphrase: "Arrrr") + + pirate.save! + + assert_equal "Squawky", parrot.reload.name + end + def test_should_automatically_validate_the_associated_models @pirate.send(@association_name).each { |child| child.name = '' } diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 4306738670..eef2d29d02 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1,7 +1,4 @@ -# -*- coding: utf-8 -*- - require "cases/helper" -require 'active_support/concurrency/latch' require 'models/post' require 'models/author' require 'models/topic' @@ -29,6 +26,7 @@ require 'models/bird' require 'models/car' require 'models/bulb' require 'rexml/document' +require 'concurrent/atomic/count_down_latch' class FirstAbstractClass < ActiveRecord::Base self.abstract_class = true @@ -84,7 +82,6 @@ class BasicsTest < ActiveRecord::TestCase classname = conn.class.name[/[^:]*$/] badchar = { 'SQLite3Adapter' => '"', - 'MysqlAdapter' => '`', 'Mysql2Adapter' => '`', 'PostgreSQLAdapter' => '"', 'OracleAdapter' => '"', @@ -114,7 +111,9 @@ class BasicsTest < ActiveRecord::TestCase unless current_adapter?(:PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter, :FbAdapter) def test_limit_with_comma - assert Topic.limit("1,2").to_a + assert_deprecated do + assert Topic.limit("1,2").to_a + end end end @@ -140,14 +139,10 @@ class BasicsTest < ActiveRecord::TestCase end def test_limit_should_sanitize_sql_injection_for_limit_with_commas - assert_raises(ArgumentError) do - Topic.limit("1, 7 procedure help()").to_a - end - end - - unless current_adapter?(:MysqlAdapter, :Mysql2Adapter) - def test_limit_should_allow_sql_literal - assert_equal 1, Topic.limit(Arel.sql('2-1')).to_a.length + assert_deprecated do + assert_raises(ArgumentError) do + Topic.limit("1, 7 procedure help()").to_a + end end end @@ -206,7 +201,7 @@ class BasicsTest < ActiveRecord::TestCase ) # For adapters which support microsecond resolution. - if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) || mysql_56? + if subsecond_precision_supported? assert_equal 11, Topic.find(1).written_on.sec assert_equal 223300, Topic.find(1).written_on.usec assert_equal 9900, Topic.find(2).written_on.usec @@ -215,7 +210,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc - with_env_tz 'America/New_York' do + with_env_tz eastern_time_zone do with_timezone_config default: :utc do time = Time.local(2000) topic = Topic.create('written_on' => time) @@ -228,7 +223,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_utc - with_env_tz 'America/New_York' do + with_env_tz eastern_time_zone do with_timezone_config default: :utc do Time.use_zone 'Central Time (US & Canada)' do time = Time.zone.local(2000) @@ -243,7 +238,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_preserving_time_objects_with_utc_time_conversion_to_default_timezone_local - with_env_tz 'America/New_York' do + with_env_tz eastern_time_zone do with_timezone_config default: :local do time = Time.utc(2000) topic = Topic.create('written_on' => time) @@ -256,7 +251,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_local - with_env_tz 'America/New_York' do + with_env_tz eastern_time_zone do with_timezone_config default: :local do Time.use_zone 'Central Time (US & Canada)' do time = Time.zone.local(2000) @@ -270,6 +265,14 @@ class BasicsTest < ActiveRecord::TestCase end end + def eastern_time_zone + if Gem.win_platform? + "EST5EDT" + else + "America/New_York" + end + end + def test_custom_mutator topic = Topic.find(1) # This mutator is protected in the class definition @@ -440,7 +443,7 @@ class BasicsTest < ActiveRecord::TestCase Post.reset_table_name end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_update_all_with_order_and_limit assert_equal 1, Topic.limit(1).order('id DESC').update_all(:content => 'bulk updated!') end @@ -521,7 +524,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_find_by_slug_with_array - assert_equal Topic.find(['1-meowmeow', '2-hello']), Topic.find([1, 2]) + assert_equal Topic.find([1, 2]), Topic.find(['1-meowmeow', '2-hello']) + assert_equal 'The Second Topic of the day', Topic.find(['2-hello', '1-meowmeow']).first.title end def test_find_by_slug_with_range @@ -800,7 +804,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal dev.salary.amount, dup.salary.amount assert !dup.persisted? - # test if the attributes have been dupd + # test if the attributes have been duped original_amount = dup.salary.amount dev.salary.amount = 1 assert_equal original_amount, dup.salary.amount @@ -946,6 +950,34 @@ class BasicsTest < ActiveRecord::TestCase assert_equal BigDecimal("1000234000567.95"), m1.big_bank_balance end + def test_numeric_fields_with_scale + m = NumericData.new( + :bank_balance => 1586.43122334, + :big_bank_balance => BigDecimal("234000567.952344"), + :world_population => 6000000000, + :my_house_population => 3 + ) + assert m.save + + m1 = NumericData.find(m.id) + assert_not_nil m1 + + # As with migration_test.rb, we should make world_population >= 2**62 + # to cover 64-bit platforms and test it is a Bignum, but the main thing + # is that it's an Integer. + assert_kind_of Integer, m1.world_population + assert_equal 6000000000, m1.world_population + + assert_kind_of Fixnum, m1.my_house_population + assert_equal 3, m1.my_house_population + + assert_kind_of BigDecimal, m1.bank_balance + assert_equal BigDecimal("1586.43"), m1.bank_balance + + assert_kind_of BigDecimal, m1.big_bank_balance + assert_equal BigDecimal("234000567.95"), m1.big_bank_balance + end + def test_auto_id auto = AutoId.new auto.save @@ -1178,42 +1210,10 @@ class BasicsTest < ActiveRecord::TestCase assert_equal last, Developer.all.merge!(:order => :salary).to_a.last end - def test_abstract_class - assert !ActiveRecord::Base.abstract_class? - assert LoosePerson.abstract_class? - assert !LooseDescendant.abstract_class? - end - def test_abstract_class_table_name assert_nil AbstractCompany.table_name end - def test_descends_from_active_record - assert !ActiveRecord::Base.descends_from_active_record? - - # Abstract subclass of AR::Base. - assert LoosePerson.descends_from_active_record? - - # Concrete subclass of an abstract class. - assert LooseDescendant.descends_from_active_record? - - # Concrete subclass of AR::Base. - assert TightPerson.descends_from_active_record? - - # Concrete subclass of a concrete class but has no type column. - assert TightDescendant.descends_from_active_record? - - # Concrete subclass of AR::Base. - assert Post.descends_from_active_record? - - # Abstract subclass of a concrete class which has a type column. - # This is pathological, as you'll never have Sub < Abstract < Concrete. - assert !StiPost.descends_from_active_record? - - # Concrete subclasses an abstract class which has a type column. - assert !SubStiPost.descends_from_active_record? - end - def test_find_on_abstract_base_class_doesnt_use_type_condition old_class = LooseDescendant Object.send :remove_const, :LooseDescendant @@ -1252,56 +1252,13 @@ class BasicsTest < ActiveRecord::TestCase original_logger = ActiveRecord::Base.logger log = StringIO.new ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) + ActiveRecord::Base.logger.level = Logger::DEBUG ActiveRecord::Base.benchmark("Logging", :level => :debug, :silence => false) { ActiveRecord::Base.logger.debug "Quiet" } assert_match(/Quiet/, log.string) ensure ActiveRecord::Base.logger = original_logger end - def test_compute_type_success - assert_equal Author, ActiveRecord::Base.send(:compute_type, 'Author') - end - - def test_compute_type_nonexistent_constant - e = assert_raises NameError do - ActiveRecord::Base.send :compute_type, 'NonexistentModel' - end - assert_equal 'uninitialized constant ActiveRecord::Base::NonexistentModel', e.message - assert_equal 'ActiveRecord::Base::NonexistentModel', e.name - end - - def test_compute_type_no_method_error - ActiveSupport::Dependencies.stubs(:safe_constantize).raises(NoMethodError) - assert_raises NoMethodError do - ActiveRecord::Base.send :compute_type, 'InvalidModel' - end - end - - def test_compute_type_on_undefined_method - error = nil - begin - Class.new(Author) do - alias_method :foo, :bar - end - rescue => e - error = e - end - - ActiveSupport::Dependencies.stubs(:safe_constantize).raises(e) - - exception = assert_raises NameError do - ActiveRecord::Base.send :compute_type, 'InvalidModel' - end - assert_equal error.message, exception.message - end - - def test_compute_type_argument_error - ActiveSupport::Dependencies.stubs(:safe_constantize).raises(ArgumentError) - assert_raises ArgumentError do - ActiveRecord::Base.send :compute_type, 'InvalidModel' - end - end - def test_clear_cache! # preheat cache c1 = Post.connection.schema_cache.columns('posts') @@ -1318,9 +1275,10 @@ class BasicsTest < ActiveRecord::TestCase UnloadablePost.send(:current_scope=, UnloadablePost.all) UnloadablePost.unloadable - assert_not_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, "UnloadablePost") + klass = UnloadablePost + assert_not_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, klass) ActiveSupport::Dependencies.remove_unloadable_constants! - assert_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, "UnloadablePost") + assert_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, klass) ensure Object.class_eval{ remove_const :UnloadablePost } if defined?(UnloadablePost) end @@ -1395,6 +1353,19 @@ class BasicsTest < ActiveRecord::TestCase Company.attribute_names end + def test_has_attribute + assert Company.has_attribute?('id') + assert Company.has_attribute?('type') + assert Company.has_attribute?('name') + assert_not Company.has_attribute?('lastname') + assert_not Company.has_attribute?('age') + end + + def test_has_attribute_with_symbol + assert Company.has_attribute?(:id) + assert_not Company.has_attribute?(:age) + end + def test_attribute_names_on_table_not_exists assert_equal [], NonExistentTable.attribute_names end @@ -1411,15 +1382,13 @@ class BasicsTest < ActiveRecord::TestCase end def test_uniq_delegates_to_scoped - scope = stub - Bird.stubs(:all).returns(mock(:uniq => scope)) - assert_equal scope, Bird.uniq + assert_deprecated do + assert_equal Bird.all.distinct, Bird.uniq + end end def test_distinct_delegates_to_scoped - scope = stub - Bird.stubs(:all).returns(mock(:distinct => scope)) - assert_equal scope, Bird.distinct + assert_equal Bird.all.distinct, Bird.distinct end def test_table_name_with_2_abstract_subclasses @@ -1508,20 +1477,20 @@ class BasicsTest < ActiveRecord::TestCase orig_handler = klass.connection_handler new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new after_handler = nil - latch1 = ActiveSupport::Concurrency::Latch.new - latch2 = ActiveSupport::Concurrency::Latch.new + latch1 = Concurrent::CountDownLatch.new + latch2 = Concurrent::CountDownLatch.new t = Thread.new do klass.connection_handler = new_handler - latch1.release - latch2.await + latch1.count_down + latch2.wait after_handler = klass.connection_handler end - latch1.await + latch1.wait klass.connection_handler = orig_handler - latch2.release + latch2.count_down t.join assert_equal after_handler, new_handler @@ -1544,4 +1513,22 @@ class BasicsTest < ActiveRecord::TestCase assert_not topic.id_changed? end + + test "ignored columns are not present in columns_hash" do + cache_columns = Developer.connection.schema_cache.columns_hash(Developer.table_name) + assert_includes cache_columns.keys, 'first_name' + refute_includes Developer.columns_hash.keys, 'first_name' + end + + test "ignored columns have no attribute methods" do + refute Developer.new.respond_to?(:first_name) + refute Developer.new.respond_to?(:first_name=) + refute Developer.new.respond_to?(:first_name?) + end + + test "ignored columns don't prevent explicit declaration of attribute methods" do + assert Developer.new.respond_to?(:last_name) + assert Developer.new.respond_to?(:last_name=) + assert Developer.new.respond_to?(:last_name?) + end end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 9e428098e4..91ff5146fd 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -38,7 +38,7 @@ class EachTest < ActiveRecord::TestCase if Enumerator.method_defined? :size def test_each_should_return_a_sized_enumerator assert_equal 11, Post.find_each(batch_size: 1).size - assert_equal 5, Post.find_each(batch_size: 2, begin_at: 7).size + assert_equal 5, Post.find_each(batch_size: 2, start: 7).size assert_equal 11, Post.find_each(batch_size: 10_000).size end end @@ -53,7 +53,7 @@ class EachTest < ActiveRecord::TestCase end def test_each_should_raise_if_select_is_set_without_id - assert_raise(RuntimeError) do + assert_raise(ArgumentError) do Post.select(:title).find_each(batch_size: 1) { |post| flunk "should not call this block" } @@ -69,13 +69,15 @@ class EachTest < ActiveRecord::TestCase end def test_warn_if_limit_scope_is_set - ActiveRecord::Base.logger.expects(:warn) - Post.limit(1).find_each { |post| post } + assert_called(ActiveRecord::Base.logger, :warn) do + Post.limit(1).find_each { |post| post } + end end def test_warn_if_order_scope_is_set - ActiveRecord::Base.logger.expects(:warn) - Post.order("title").find_each { |post| post } + assert_called(ActiveRecord::Base.logger, :warn) do + Post.order("title").find_each { |post| post } + end end def test_logger_not_required @@ -99,16 +101,16 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_start_from_the_start_option assert_queries(@total) do - Post.find_in_batches(batch_size: 1, begin_at: 2) do |batch| + Post.find_in_batches(batch_size: 1, start: 2) do |batch| assert_kind_of Array, batch assert_kind_of Post, batch.first end end end - def test_find_in_batches_should_end_at_the_end_option + def test_find_in_batches_should_end_at_the_finish_option assert_queries(6) do - Post.find_in_batches(batch_size: 1, end_at: 5) do |batch| + Post.find_in_batches(batch_size: 1, finish: 5) do |batch| assert_kind_of Array, batch assert_kind_of Post, batch.first end @@ -137,14 +139,15 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified not_a_post = "not a post" - not_a_post.stubs(:id).raises(StandardError, "not_a_post had #id called on it") - - assert_nothing_raised do - Post.find_in_batches(:batch_size => 1) do |batch| - assert_kind_of Array, batch - assert_kind_of Post, batch.first + def not_a_post.id; end + not_a_post.stub(:id, ->{ raise StandardError.new("not_a_post had #id called on it") }) do + assert_nothing_raised do + Post.find_in_batches(:batch_size => 1) do |batch| + assert_kind_of Array, batch + assert_kind_of Post, batch.first - batch.map! { not_a_post } + batch.map! { not_a_post } + end end end end @@ -158,7 +161,43 @@ class EachTest < ActiveRecord::TestCase end # posts.first will be ordered using id only. Title order scope should not apply here assert_not_equal first_post, posts.first - assert_equal posts(:welcome), posts.first + assert_equal posts(:welcome).id, posts.first.id + end + + def test_find_in_batches_should_error_on_ignore_the_order + assert_raise(ArgumentError) do + PostWithDefaultScope.find_in_batches(error_on_ignore: true){} + end + end + + def test_find_in_batches_should_not_error_if_config_overriden + # Set the config option which will be overriden + prev = ActiveRecord::Base.error_on_ignored_order_or_limit + ActiveRecord::Base.error_on_ignored_order_or_limit = true + assert_nothing_raised do + PostWithDefaultScope.find_in_batches(error_on_ignore: false){} + end + ensure + # Set back to default + ActiveRecord::Base.error_on_ignored_order_or_limit = prev + end + + def test_find_in_batches_should_error_on_config_specified_to_error + # Set the config option + prev = ActiveRecord::Base.error_on_ignored_order_or_limit + ActiveRecord::Base.error_on_ignored_order_or_limit = true + assert_raise(ArgumentError) do + PostWithDefaultScope.find_in_batches(){} + end + ensure + # Set back to default + ActiveRecord::Base.error_on_ignored_order_or_limit = prev + end + + def test_find_in_batches_should_not_error_by_default + assert_nothing_raised do + PostWithDefaultScope.find_in_batches(){} + end end def test_find_in_batches_should_not_ignore_the_default_scope_if_it_is_other_then_order @@ -172,7 +211,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_not_modify_passed_options assert_nothing_raised do - Post.find_in_batches({ batch_size: 42, begin_at: 1 }.freeze){} + Post.find_in_batches({ batch_size: 42, start: 1 }.freeze){} end end @@ -181,7 +220,7 @@ class EachTest < ActiveRecord::TestCase start_nick = nick_order_subscribers.second.nick subscribers = [] - Subscriber.find_in_batches(batch_size: 1, begin_at: start_nick) do |batch| + Subscriber.find_in_batches(batch_size: 1, start: start_nick) do |batch| subscribers.concat(batch) end @@ -190,15 +229,16 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified assert_queries(Subscriber.count + 1) do - Subscriber.find_each(:batch_size => 1) do |subscriber| - assert_kind_of Subscriber, subscriber + Subscriber.find_in_batches(batch_size: 1) do |batch| + assert_kind_of Array, batch + assert_kind_of Subscriber, batch.first end end end def test_find_in_batches_should_return_an_enumerator enum = nil - assert_queries(0) do + assert_no_queries do enum = Post.find_in_batches(:batch_size => 1) end assert_queries(4) do @@ -209,32 +249,239 @@ class EachTest < ActiveRecord::TestCase end end - def test_find_in_batches_start_deprecated - assert_deprecated do - assert_queries(@total) do - Post.find_in_batches(batch_size: 1, start: 2) do |batch| - assert_kind_of Array, batch - assert_kind_of Post, batch.first - end + def test_in_batches_should_not_execute_any_query + assert_no_queries do + assert_kind_of ActiveRecord::Batches::BatchEnumerator, Post.in_batches(of: 2) + end + end + + def test_in_batches_should_yield_relation_if_block_given + assert_queries(6) do + Post.in_batches(of: 2) do |relation| + assert_kind_of ActiveRecord::Relation, relation end end end - def test_find_each_start_deprecated - assert_deprecated do - assert_queries(@total) do - Post.find_each(batch_size: 1, start: 2) do |post| - assert_kind_of Post, post - end + def test_in_batches_should_be_enumerable_if_no_block_given + assert_queries(6) do + Post.in_batches(of: 2).each do |relation| + assert_kind_of ActiveRecord::Relation, relation end end end + def test_in_batches_each_record_should_yield_record_if_block_is_given + assert_queries(6) do + Post.in_batches(of: 2).each_record do |post| + assert post.title.present? + assert_kind_of Post, post + end + end + end + + def test_in_batches_each_record_should_return_enumerator_if_no_block_given + assert_queries(6) do + Post.in_batches(of: 2).each_record.with_index do |post, i| + assert post.title.present? + assert_kind_of Post, post + end + end + end + + def test_in_batches_each_record_should_be_ordered_by_id + ids = Post.order('id ASC').pluck(:id) + assert_queries(6) do + Post.in_batches(of: 2).each_record.with_index do |post, i| + assert_equal ids[i], post.id + end + end + end + + def test_in_batches_update_all_affect_all_records + assert_queries(6 + 6) do # 6 selects, 6 updates + Post.in_batches(of: 2).update_all(title: "updated-title") + end + assert_equal Post.all.pluck(:title), ["updated-title"] * Post.count + end + + def test_in_batches_delete_all_should_not_delete_records_in_other_batches + not_deleted_count = Post.where('id <= 2').count + Post.where('id > 2').in_batches(of: 2).delete_all + assert_equal 0, Post.where('id > 2').count + assert_equal not_deleted_count, Post.count + end + + def test_in_batches_should_not_be_loaded + Post.in_batches(of: 1) do |relation| + assert_not relation.loaded? + end + + Post.in_batches(of: 1, load: false) do |relation| + assert_not relation.loaded? + end + end + + def test_in_batches_should_be_loaded + Post.in_batches(of: 1, load: true) do |relation| + assert relation.loaded? + end + end + + def test_in_batches_if_not_loaded_executes_more_queries + assert_queries(@total + 1) do + Post.in_batches(of: 1, load: false) do |relation| + assert_not relation.loaded? + end + end + end + + def test_in_batches_should_return_relations + assert_queries(@total + 1) do + Post.in_batches(of: 1) do |relation| + assert_kind_of ActiveRecord::Relation, relation + end + end + end + + def test_in_batches_should_start_from_the_start_option + post = Post.order('id ASC').where('id >= ?', 2).first + assert_queries(2) do + relation = Post.in_batches(of: 1, start: 2).first + assert_equal post, relation.first + end + end + + def test_in_batches_should_end_at_the_finish_option + post = Post.order('id DESC').where('id <= ?', 5).first + assert_queries(7) do + relation = Post.in_batches(of: 1, finish: 5, load: true).reverse_each.first + assert_equal post, relation.last + end + end + + def test_in_batches_shouldnt_execute_query_unless_needed + assert_queries(2) do + Post.in_batches(of: @total) { |relation| assert_kind_of ActiveRecord::Relation, relation } + end + + assert_queries(1) do + Post.in_batches(of: @total + 1) { |relation| assert_kind_of ActiveRecord::Relation, relation } + end + end + + def test_in_batches_should_quote_batch_order + c = Post.connection + assert_sql(/ORDER BY #{c.quote_table_name('posts')}.#{c.quote_column_name('id')}/) do + Post.in_batches(of: 1) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Post, relation.first + end + end + end + + def test_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified + not_a_post = "not a post" + def not_a_post.id + raise StandardError.new("not_a_post had #id called on it") + end + + assert_nothing_raised do + Post.in_batches(of: 1) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Post, relation.first + + relation = [not_a_post] * relation.count + end + end + end + + def test_in_batches_should_not_ignore_default_scope_without_order_statements + special_posts_ids = SpecialPostWithDefaultScope.all.map(&:id).sort + posts = [] + SpecialPostWithDefaultScope.in_batches do |relation| + posts.concat(relation) + end + assert_equal special_posts_ids, posts.map(&:id) + end + + def test_in_batches_should_not_modify_passed_options + assert_nothing_raised do + Post.in_batches({ of: 42, start: 1 }.freeze){} + end + end + + def test_in_batches_should_use_any_column_as_primary_key + nick_order_subscribers = Subscriber.order('nick asc') + start_nick = nick_order_subscribers.second.nick + + subscribers = [] + Subscriber.in_batches(of: 1, start: start_nick) do |relation| + subscribers.concat(relation) + end + + assert_equal nick_order_subscribers[1..-1].map(&:id), subscribers.map(&:id) + end + + def test_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified + assert_queries(Subscriber.count + 1) do + Subscriber.in_batches(of: 1, load: true) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Subscriber, relation.first + end + end + end + + def test_in_batches_should_return_an_enumerator + enum = nil + assert_no_queries do + enum = Post.in_batches(of: 1) + end + assert_queries(4) do + enum.first(4) do |relation| + assert_kind_of ActiveRecord::Relation, relation + assert_kind_of Post, relation.first + end + end + end + + def test_in_batches_relations_should_not_overlap_with_each_other + seen_posts = [] + Post.in_batches(of: 2, load: true) do |relation| + relation.to_a.each do |post| + assert_not seen_posts.include?(post) + seen_posts << post + end + end + end + + def test_in_batches_relations_with_condition_should_not_overlap_with_each_other + seen_posts = [] + author_id = Post.first.author_id + posts_by_author = Post.where(author_id: author_id) + Post.in_batches(of: 2) do |batch| + seen_posts += batch.where(author_id: author_id) + end + + assert_equal posts_by_author.pluck(:id).sort, seen_posts.map(&:id).sort + end + + def test_in_batches_relations_update_all_should_not_affect_matching_records_in_other_batches + Post.update_all(author_id: 0) + person = Post.last + person.update_attributes(author_id: 1) + + Post.in_batches(of: 2) do |batch| + batch.where('author_id >= 1').update_all('author_id = author_id + 1') + end + assert_equal 2, person.reload.author_id # incremented only once + end + if Enumerator.method_defined? :size def test_find_in_batches_should_return_a_sized_enumerator assert_equal 11, Post.find_in_batches(:batch_size => 1).size assert_equal 6, Post.find_in_batches(:batch_size => 2).size - assert_equal 4, Post.find_in_batches(batch_size: 2, begin_at: 4).size + assert_equal 4, Post.find_in_batches(batch_size: 2, start: 4).size assert_equal 4, Post.find_in_batches(:batch_size => 3).size assert_equal 1, Post.find_in_batches(:batch_size => 10_000).size end diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index 86dee929bf..9eb5352150 100644 --- a/activerecord/test/cases/binary_test.rb +++ b/activerecord/test/cases/binary_test.rb @@ -20,10 +20,6 @@ unless current_adapter?(:DB2Adapter) name = binary.name - # MySQL adapter doesn't properly encode things, so we have to do it - if current_adapter?(:MysqlAdapter) - name.force_encoding(Encoding::UTF_8) - end assert_equal 'いただきます!', name end diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 1e38b97c4a..fa924fe4cb 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -31,7 +31,8 @@ module ActiveRecord ActiveSupport::Notifications.unsubscribe(@subscription) end - if ActiveRecord::Base.connection.supports_statement_cache? + if ActiveRecord::Base.connection.supports_statement_cache? && + ActiveRecord::Base.connection.prepared_statements def test_bind_from_join_in_subquery subquery = Author.joins(:thinking_posts).where(name: 'David') scope = Author.from(subquery, 'authors').where(id: 1) @@ -39,7 +40,7 @@ module ActiveRecord end def test_binds_are_logged - sub = @connection.substitute_at(@pk) + sub = Arel::Nodes::BindParam.new binds = [Relation::QueryAttribute.new("id", 1, Type::Value.new)] sql = "select * from topics where id = #{sub.to_sql}" diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb new file mode 100644 index 0000000000..bb2829b3c1 --- /dev/null +++ b/activerecord/test/cases/cache_key_test.rb @@ -0,0 +1,25 @@ +require "cases/helper" + +module ActiveRecord + class CacheKeyTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + class CacheMe < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:cache_mes) { |t| t.timestamps } + end + + teardown do + @connection.drop_table :cache_mes, if_exists: true + end + + test "test_cache_key_format_is_not_too_precise" do + record = CacheMe.create + key = record.cache_key + + assert_equal key, record.reload.cache_key + end + end +end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index f0393aa6b1..cfae700159 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -11,6 +11,10 @@ require 'models/minivan' require 'models/speedometer' require 'models/ship_part' require 'models/treasure' +require 'models/developer' +require 'models/comment' +require 'models/rating' +require 'models/post' class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' @@ -27,11 +31,20 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 318, Account.sum(:credit_limit) end + def test_should_sum_arel_attribute + assert_equal 318, Account.sum(Account.arel_table[:credit_limit]) + end + def test_should_average_field value = Account.average(:credit_limit) assert_equal 53.0, value end + def test_should_average_arel_attribute + value = Account.average(Account.arel_table[:credit_limit]) + assert_equal 53.0, value + end + def test_should_resolve_aliased_attributes assert_equal 318, Account.sum(:available_credit) end @@ -56,14 +69,26 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 60, Account.maximum(:credit_limit) end + def test_should_get_maximum_of_arel_attribute + assert_equal 60, Account.maximum(Account.arel_table[:credit_limit]) + end + def test_should_get_maximum_of_field_with_include assert_equal 55, Account.where("companies.name != 'Summit'").references(:companies).includes(:firm).maximum(:credit_limit) end + def test_should_get_maximum_of_arel_attribute_with_include + assert_equal 55, Account.where("companies.name != 'Summit'").references(:companies).includes(:firm).maximum(Account.arel_table[:credit_limit]) + end + def test_should_get_minimum_of_field assert_equal 50, Account.minimum(:credit_limit) end + def test_should_get_minimum_of_arel_attribute + assert_equal 50, Account.minimum(Account.arel_table[:credit_limit]) + end + def test_should_group_by_field c = Account.group(:firm_id).sum(:credit_limit) [1,6,2].each do |firm_id| @@ -98,6 +123,25 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 60, c[2] end + def test_should_generate_valid_sql_with_joins_and_group + assert_nothing_raised do + AuditLog.joins(:developer).group(:id).count + end + end + + def test_should_calculate_against_given_relation + developer = Developer.create!(name: "developer") + developer.audit_logs.create!(message: "first log") + developer.audit_logs.create!(message: "second log") + + c = developer.audit_logs.joins(:developer).group(:id).count + + assert_equal developer.audit_logs.count, c.size + developer.audit_logs.each do |log| + assert_equal 1, c[log.id] + end + end + def test_should_order_by_grouped_field c = Account.group(:firm_id).order("firm_id").sum(:credit_limit) assert_equal [1, 2, 6, 9], c.keys.compact @@ -127,6 +171,14 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 3, accounts.select(:firm_id).count end + def test_limit_should_apply_before_count_arel_attribute + accounts = Account.limit(3).where('firm_id IS NOT NULL') + + firm_id_attribute = Account.arel_table[:firm_id] + assert_equal 3, accounts.count(firm_id_attribute) + assert_equal 3, accounts.select(firm_id_attribute).count + end + def test_count_should_shortcut_with_limit_zero accounts = Account.limit(0) @@ -349,13 +401,29 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 6, Account.select("DISTINCT accounts.id").includes(:firm).count end + def test_count_selected_arel_attribute + assert_equal 5, Account.select(Account.arel_table[:firm_id]).count + assert_equal 4, Account.distinct.select(Account.arel_table[:firm_id]).count + end + def test_count_with_column_parameter assert_equal 5, Account.count(:firm_id) end + def test_count_with_arel_attribute + assert_equal 5, Account.count(Account.arel_table[:firm_id]) + end + + def test_count_with_arel_star + assert_equal 6, Account.count(Arel.star) + end + def test_count_with_distinct assert_equal 4, Account.select(:credit_limit).distinct.count - assert_equal 4, Account.select(:credit_limit).uniq.count + + assert_deprecated do + assert_equal 4, Account.select(:credit_limit).uniq.count + end end def test_count_with_aliased_attribute @@ -371,12 +439,27 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 4, Account.joins(:firm).distinct.count('companies.id') end + def test_count_arel_attribute_in_joined_table_with + assert_equal 5, Account.joins(:firm).count(Company.arel_table[:id]) + assert_equal 4, Account.joins(:firm).distinct.count(Company.arel_table[:id]) + end + + def test_count_selected_arel_attribute_in_joined_table + assert_equal 5, Account.joins(:firm).select(Company.arel_table[:id]).count + assert_equal 4, Account.joins(:firm).distinct.select(Company.arel_table[:id]).count + end + def test_should_count_field_in_joined_table_with_group_by c = Account.group('accounts.firm_id').joins(:firm).count('companies.id') [1,6,2,9].each { |firm_id| assert c.keys.include?(firm_id) } end + def test_should_count_field_of_root_table_with_conflicting_group_by_column + assert_equal({ 1 => 1 }, Firm.joins(:accounts).group(:firm_id).count) + assert_equal({ 1 => 1 }, Firm.joins(:accounts).group('accounts.firm_id').count) + end + def test_count_with_no_parameters_isnt_deprecated assert_not_deprecated { Account.count } end @@ -399,6 +482,10 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 1, Account.where(firm_name: '37signals').order(:firm_name).reverse_order.count end + def test_count_with_block + assert_equal 4, Account.count { |account| account.credit_limit.modulo(10).zero? } + end + def test_should_sum_expression # Oracle adapter returns floating point value 636.0 after SUM if current_adapter?(:OracleAdapter) @@ -462,8 +549,8 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 7, Company.includes(:contracts).sum(:developer_id) end - def test_from_option_with_specified_index - if Edge.connection.adapter_name == 'MySQL' or Edge.connection.adapter_name == 'Mysql2' + if current_adapter?(:Mysql2Adapter) + def test_from_option_with_specified_index assert_equal Edge.count(:all), Edge.from('edges USE INDEX(unique_edge_index)').count(:all) assert_equal Edge.where('sink_id < 5').count(:all), Edge.from('edges USE INDEX(unique_edge_index)').where('sink_id < 5').count(:all) @@ -500,8 +587,8 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [ topic.written_on ], relation.pluck(:written_on) end - def test_pluck_and_uniq - assert_equal [50, 53, 55, 60], Account.order(:credit_limit).uniq.pluck(:credit_limit) + def test_pluck_and_distinct + assert_equal [50, 53, 55, 60], Account.order(:credit_limit).distinct.pluck(:credit_limit) end def test_pluck_in_relation @@ -625,6 +712,27 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [part.id], ShipPart.joins(:trinkets).pluck(:id) end + def test_pluck_loaded_relation + companies = Company.order(:id).limit(3).load + assert_no_queries do + assert_equal ['37signals', 'Summit', 'Microsoft'], companies.pluck(:name) + end + end + + def test_pluck_loaded_relation_multiple_columns + companies = Company.order(:id).limit(3).load + assert_no_queries do + assert_equal [[1, '37signals'], [2, 'Summit'], [3, 'Microsoft']], companies.pluck(:id, :name) + end + end + + def test_pluck_loaded_relation_sql_fragment + companies = Company.order(:name).limit(3).load + assert_queries 1 do + assert_equal ['37signals', 'Apex', 'Ex Nihilo'], companies.pluck('DISTINCT name') + end + end + def test_grouped_calculation_with_polymorphic_relation part = ShipPart.create!(name: "has trinket") part.trinkets.create! @@ -636,4 +744,53 @@ class CalculationsTest < ActiveRecord::TestCase Client.update_all(client_of: nil) assert_equal({ nil => Client.count }, Client.group(:firm).count) end + + def test_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association + assert_nothing_raised do + developer = Developer.create!(name: 'developer') + developer.ratings.includes(comment: :post).where(posts: { id: 1 }).count + end + end + + def test_sum_uses_enumerable_version_when_block_is_given + block_called = false + relation = Client.all.load + + assert_no_queries do + assert_equal 0, relation.sum { block_called = true; 0 } + end + assert block_called + end + + def test_having_with_strong_parameters + protected_params = Class.new do + attr_reader :permitted + alias :permitted? :permitted + + def initialize(parameters) + @parameters = parameters + @permitted = false + end + + def to_h + @parameters + end + + def permit! + @permitted = true + self + end + end + + params = protected_params.new(credit_limit: '50') + + assert_raises(ActiveModel::ForbiddenAttributesError) do + Account.group(:id).having(params) + end + + result = Account.group(:id).having(params.permit!) + assert_equal 50, result[0].credit_limit + assert_equal 50, result[1].credit_limit + assert_equal 50, result[2].credit_limit + end end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 3ae4a6eade..4f70ae3a1d 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -33,7 +33,7 @@ class CallbackDeveloper < ActiveRecord::Base ActiveRecord::Callbacks::CALLBACKS.each do |callback_method| next if callback_method.to_s =~ /^around_/ define_callback_method(callback_method) - send(callback_method, callback_string(callback_method)) + ActiveSupport::Deprecation.silence { send(callback_method, callback_string(callback_method)) } send(callback_method, callback_proc(callback_method)) send(callback_method, callback_object(callback_method)) send(callback_method) { |model| model.history << [callback_method, :block] } @@ -451,6 +451,7 @@ class CallbacksTest < ActiveRecord::TestCase assert !david.save exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } assert_equal exc.record, david + assert_equal "Failed to save the record", exc.message end david = ImmutableDeveloper.find(1) @@ -494,6 +495,7 @@ class CallbacksTest < ActiveRecord::TestCase assert !david.destroy exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } assert_equal exc.record, david + assert_equal "Failed to destroy the record", exc.message end assert_not_nil ImmutableDeveloper.find_by_id(1) diff --git a/activerecord/test/cases/coders/json_test.rb b/activerecord/test/cases/coders/json_test.rb new file mode 100644 index 0000000000..d22d93d129 --- /dev/null +++ b/activerecord/test/cases/coders/json_test.rb @@ -0,0 +1,15 @@ +require "cases/helper" + +module ActiveRecord + module Coders + class JSONTest < ActiveRecord::TestCase + def test_returns_nil_if_empty_string_given + assert_nil JSON.load("") + end + + def test_returns_nil_if_nil_given + assert_nil JSON.load(nil) + end + end + end +end diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb new file mode 100644 index 0000000000..a2874438c1 --- /dev/null +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -0,0 +1,89 @@ +require "cases/helper" +require "models/computer" +require "models/developer" +require "models/project" +require "models/topic" +require "models/post" +require "models/comment" + +module ActiveRecord + class CollectionCacheKeyTest < ActiveRecord::TestCase + fixtures :developers, :projects, :developers_projects, :topics, :comments, :posts + + test "collection_cache_key on model" do + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, Developer.collection_cache_key) + end + + test "cache_key for relation" do + developers = Developer.where(name: "David") + last_developer_timestamp = developers.order(updated_at: :desc).first.updated_at + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + + /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/ =~ developers.cache_key + + assert_equal Digest::MD5.hexdigest(developers.to_sql), $1 + assert_equal developers.count.to_s, $2 + assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 + end + + test "it triggers at most one query" do + developers = Developer.where(name: "David") + + assert_queries(1) { developers.cache_key } + assert_queries(0) { developers.cache_key } + end + + test "it doesn't trigger any query if the relation is already loaded" do + developers = Developer.where(name: "David").load + assert_queries(0) { developers.cache_key } + end + + test "relation cache_key changes when the sql query changes" do + developers = Developer.where(name: "David") + other_relation = Developer.where(name: "David").where("1 = 1") + + assert_not_equal developers.cache_key, other_relation.cache_key + end + + test "cache_key for empty relation" do + developers = Developer.where(name: "Non Existent Developer") + assert_match(/\Adevelopers\/query-(\h+)-0\Z/, developers.cache_key) + end + + test "cache_key with custom timestamp column" do + topics = Topic.where("title like ?", "%Topic%") + last_topic_timestamp = topics(:fifth).written_on.utc.to_s(:usec) + assert_match(last_topic_timestamp, topics.cache_key(:written_on)) + end + + test "cache_key with unknown timestamp column" do + topics = Topic.where("title like ?", "%Topic%") + assert_raises(ActiveRecord::StatementInvalid) { topics.cache_key(:published_at) } + end + + test "collection proxy provides a cache_key" do + developers = projects(:active_record).developers + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + end + + test "cache_key for loaded collection with zero size" do + Comment.delete_all + posts = Post.includes(:comments) + empty_loaded_collection = posts.first.comments + + assert_match(/\Acomments\/query-(\h+)-0\Z/, empty_loaded_collection.cache_key) + end + + test "cache_key for queries with offset which return 0 rows" do + developers = Developer.offset(20) + assert_match(/\Adevelopers\/query-(\h+)-0\Z/, developers.cache_key) + end + + test "cache_key with a relation having selected columns" do + developers = Developer.select(:salary) + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + end + end +end diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index 14b95ecab1..81162b7e98 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -38,42 +38,52 @@ module ActiveRecord assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, @viz.accept(column_def) end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_should_set_default_for_mysql_binary_data_types type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)") - binary_column = AbstractMysqlAdapter::Column.new("title", "a", type) + binary_column = MySQL::Column.new("title", "a", type) assert_equal "a", binary_column.default type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary") - varbinary_column = AbstractMysqlAdapter::Column.new("title", "a", type) + varbinary_column = MySQL::Column.new("title", "a", type) assert_equal "a", varbinary_column.default end + def test_should_be_empty_string_default_for_mysql_binary_data_types + type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)") + binary_column = MySQL::Column.new("title", "", type, false) + assert_equal "", binary_column.default + + type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary") + varbinary_column = MySQL::Column.new("title", "", type, false) + assert_equal "", varbinary_column.default + end + def test_should_not_set_default_for_blob_and_text_data_types assert_raise ArgumentError do - AbstractMysqlAdapter::Column.new("title", "a", SqlTypeMetadata.new(sql_type: "blob")) + MySQL::Column.new("title", "a", SqlTypeMetadata.new(sql_type: "blob")) end - text_type = AbstractMysqlAdapter::MysqlTypeMetadata.new( + text_type = MySQL::TypeMetadata.new( SqlTypeMetadata.new(type: :text)) assert_raise ArgumentError do - AbstractMysqlAdapter::Column.new("title", "Hello", text_type) + MySQL::Column.new("title", "Hello", text_type) end - text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type) + text_column = MySQL::Column.new("title", nil, text_type) assert_equal nil, text_column.default - not_null_text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type, false) + not_null_text_column = MySQL::Column.new("title", nil, text_type, false) assert_equal "", not_null_text_column.default end def test_has_default_should_return_false_for_blob_and_text_data_types binary_type = SqlTypeMetadata.new(sql_type: "blob") - blob_column = AbstractMysqlAdapter::Column.new("title", nil, binary_type) + blob_column = MySQL::Column.new("title", nil, binary_type) assert !blob_column.has_default? text_type = SqlTypeMetadata.new(type: :text) - text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type) + text_column = MySQL::Column.new("title", nil, text_type) assert !text_column.has_default? end end diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb new file mode 100644 index 0000000000..839fdbe578 --- /dev/null +++ b/activerecord/test/cases/comment_test.rb @@ -0,0 +1,139 @@ +require 'cases/helper' +require 'support/schema_dumping_helper' + +if ActiveRecord::Base.connection.supports_comments? + +class CommentTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + class Commented < ActiveRecord::Base + self.table_name = 'commenteds' + end + + class BlankComment < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + + @connection.create_table('commenteds', comment: 'A table with comment', force: true) do |t| + t.string 'name', comment: 'Comment should help clarify the column purpose' + t.boolean 'obvious', comment: 'Question is: should you comment obviously named objects?' + t.string 'content' + t.index 'name', comment: %Q["Very important" index that powers all the performance.\nAnd it's fun!] + end + + @connection.create_table('blank_comments', comment: ' ', force: true) do |t| + t.string :space_comment, comment: ' ' + t.string :empty_comment, comment: '' + t.string :nil_comment, comment: nil + t.string :absent_comment + t.index :space_comment, comment: ' ' + t.index :empty_comment, comment: '' + t.index :nil_comment, comment: nil + t.index :absent_comment + end + + Commented.reset_column_information + BlankComment.reset_column_information + end + + teardown do + @connection.drop_table 'commenteds', if_exists: true + @connection.drop_table 'blank_comments', if_exists: true + end + + def test_column_created_in_block + column = Commented.columns_hash['name'] + assert_equal :string, column.type + assert_equal 'Comment should help clarify the column purpose', column.comment + end + + def test_blank_columns_created_in_block + %w[ space_comment empty_comment nil_comment absent_comment ].each do |field| + column = BlankComment.columns_hash[field] + assert_equal :string, column.type + assert_nil column.comment + end + end + + def test_blank_indexes_created_in_block + @connection.indexes('blank_comments').each do |index| + assert_nil index.comment + end + end + + def test_add_column_with_comment_later + @connection.add_column :commenteds, :rating, :integer, comment: 'I am running out of imagination' + Commented.reset_column_information + column = Commented.columns_hash['rating'] + + assert_equal :integer, column.type + assert_equal 'I am running out of imagination', column.comment + end + + def test_add_index_with_comment_later + @connection.add_index :commenteds, :obvious, name: 'idx_obvious', comment: 'We need to see obvious comments' + index = @connection.indexes('commenteds').find { |idef| idef.name == 'idx_obvious' } + assert_equal 'We need to see obvious comments', index.comment + end + + def test_add_comment_to_column + @connection.change_column :commenteds, :content, :string, comment: 'Whoa, content describes itself!' + + Commented.reset_column_information + column = Commented.columns_hash['content'] + + assert_equal :string, column.type + assert_equal 'Whoa, content describes itself!', column.comment + end + + def test_remove_comment_from_column + @connection.change_column :commenteds, :obvious, :string, comment: nil + + Commented.reset_column_information + column = Commented.columns_hash['obvious'] + + assert_equal :string, column.type + assert_nil column.comment + end + + def test_schema_dump_with_comments + # Do all the stuff from other tests + @connection.add_column :commenteds, :rating, :integer, comment: 'I am running out of imagination' + @connection.change_column :commenteds, :content, :string, comment: 'Whoa, content describes itself!' + @connection.change_column :commenteds, :obvious, :string, comment: nil + @connection.add_index :commenteds, :obvious, name: 'idx_obvious', comment: 'We need to see obvious comments' + + # And check that these changes are reflected in dump + output = dump_table_schema 'commenteds' + assert_match %r[create_table "commenteds",.+\s+comment: "A table with comment"], output + assert_match %r[t\.string\s+"name",\s+comment: "Comment should help clarify the column purpose"], output + assert_match %r[t\.string\s+"obvious"\n], output + assert_match %r[t\.string\s+"content",\s+comment: "Whoa, content describes itself!"], output + assert_match %r[t\.integer\s+"rating",\s+comment: "I am running out of imagination"], output + assert_match %r[t\.index\s+.+\s+comment: "\\\"Very important\\\" index that powers all the performance.\\nAnd it's fun!"], output + assert_match %r[t\.index\s+.+\s+name: "idx_obvious",.+\s+comment: "We need to see obvious comments"], output + end + + def test_schema_dump_omits_blank_comments + output = dump_table_schema 'blank_comments' + + assert_match %r[create_table "blank_comments"], output + assert_no_match %r[create_table "blank_comments",.+comment:], output + + assert_match %r[t\.string\s+"space_comment"\n], output + assert_no_match %r[t\.string\s+"space_comment", comment:\n], output + + assert_match %r[t\.string\s+"empty_comment"\n], output + assert_no_match %r[t\.string\s+"empty_comment", comment:\n], output + + assert_match %r[t\.string\s+"nil_comment"\n], output + assert_no_match %r[t\.string\s+"nil_comment", comment:\n], output + + assert_match %r[t\.string\s+"absent_comment"\n], output + assert_no_match %r[t\.string\s+"absent_comment", comment:\n], output + end +end + +end diff --git a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb index 662e19f35e..c7ca428ab7 100644 --- a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb +++ b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb @@ -6,7 +6,7 @@ module ActiveRecord class Pool < ConnectionPool def insert_connection_for_test!(c) synchronize do - @connections << c + adopt_connection(c) @available.add c end end @@ -24,7 +24,9 @@ module ActiveRecord def test_lease_twice assert @adapter.lease, 'should lease adapter' - assert_not @adapter.lease, 'should not lease adapter' + assert_raises(ActiveRecordError) do + @adapter.lease + end end def test_expire_mutates_in_use @@ -35,7 +37,7 @@ module ActiveRecord end def test_close - pool = Pool.new(ConnectionSpecification.new({}, nil)) + pool = Pool.new(ConnectionSpecification.new("primary", {}, nil)) pool.insert_connection_for_test! @adapter @adapter.pool = pool diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index b72f8ca88c..50f942f5aa 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -4,47 +4,121 @@ module ActiveRecord module ConnectionAdapters class ConnectionHandlerTest < ActiveRecord::TestCase def setup - @klass = Class.new(Base) { def self.name; 'klass'; end } - @subklass = Class.new(@klass) { def self.name; 'subklass'; end } - @handler = ConnectionHandler.new - @pool = @handler.establish_connection(@klass, Base.connection_pool.spec) + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new Base.configurations + @spec_name = "primary" + @pool = @handler.establish_connection(resolver.spec(:arunit, @spec_name)) + end + + def test_establish_connection_uses_spec_name + config = {"readonly" => {"adapter" => 'sqlite3'}} + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(config) + spec = resolver.spec(:readonly) + @handler.establish_connection(spec) + + assert_not_nil @handler.retrieve_connection_pool('readonly') + ensure + @handler.remove_connection('readonly') end def test_retrieve_connection - assert @handler.retrieve_connection(@klass) + assert @handler.retrieve_connection(@spec_name) end def test_active_connections? assert !@handler.active_connections? - assert @handler.retrieve_connection(@klass) + assert @handler.retrieve_connection(@spec_name) assert @handler.active_connections? @handler.clear_active_connections! assert !@handler.active_connections? end - def test_retrieve_connection_pool_with_ar_base - assert_nil @handler.retrieve_connection_pool(ActiveRecord::Base) + def test_retrieve_connection_pool + assert_not_nil @handler.retrieve_connection_pool(@spec_name) end - def test_retrieve_connection_pool - assert_not_nil @handler.retrieve_connection_pool(@klass) + def test_retrieve_connection_pool_with_invalid_id + assert_nil @handler.retrieve_connection_pool("foo") end - def test_retrieve_connection_pool_uses_superclass_when_no_subclass_connection - assert_not_nil @handler.retrieve_connection_pool(@subklass) + def test_connection_pools + assert_equal([@pool], @handler.connection_pools) end - def test_retrieve_connection_pool_uses_superclass_pool_after_subclass_establish_and_remove - sub_pool = @handler.establish_connection(@subklass, Base.connection_pool.spec) - assert_same sub_pool, @handler.retrieve_connection_pool(@subklass) + if Process.respond_to?(:fork) + def test_connection_pool_per_pid + object_id = ActiveRecord::Base.connection.object_id - @handler.remove_connection @subklass - assert_same @pool, @handler.retrieve_connection_pool(@subklass) - end + rd, wr = IO.pipe + rd.binmode + wr.binmode - def test_connection_pools - assert_equal([@pool], @handler.connection_pools) + pid = fork { + rd.close + wr.write Marshal.dump ActiveRecord::Base.connection.object_id + wr.close + exit! + } + + wr.close + + Process.waitpid pid + assert_not_equal object_id, Marshal.load(rd.read) + rd.close + end + + def test_retrieve_connection_pool_copies_schema_cache_from_ancestor_pool + @pool.schema_cache = @pool.connection.schema_cache + @pool.schema_cache.add('posts') + + rd, wr = IO.pipe + rd.binmode + wr.binmode + + pid = fork { + rd.close + pool = @handler.retrieve_connection_pool(@spec_name) + wr.write Marshal.dump pool.schema_cache.size + wr.close + exit! + } + + wr.close + + Process.waitpid pid + assert_equal @pool.schema_cache.size, Marshal.load(rd.read) + rd.close + end + + def test_a_class_using_custom_pool_and_switching_back_to_primary + klass2 = Class.new(Base) { def self.name; 'klass2'; end } + + assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id + + pool = klass2.establish_connection(ActiveRecord::Base.connection_pool.spec.config) + assert_equal klass2.connection.object_id, pool.connection.object_id + refute_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id + + klass2.remove_connection + + assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id + end + + def test_connection_specification_name_should_fallback_to_parent + klassA = Class.new(Base) + klassB = Class.new(klassA) + + assert_equal klassB.connection_specification_name, klassA.connection_specification_name + klassA.connection_specification_name = "readonly" + assert_equal "readonly", klassB.connection_specification_name + end + + def test_remove_connection_should_not_remove_parent + klass2 = Class.new(Base) { def self.name; 'klass2'; end } + klass2.remove_connection + refute_nil ActiveRecord::Base.connection.object_id + assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id + end end end end diff --git a/activerecord/test/cases/connection_adapters/connection_specification_test.rb b/activerecord/test/cases/connection_adapters/connection_specification_test.rb index ea2196cda2..d204fce59f 100644 --- a/activerecord/test/cases/connection_adapters/connection_specification_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_specification_test.rb @@ -4,7 +4,7 @@ module ActiveRecord module ConnectionAdapters class ConnectionSpecificationTest < ActiveRecord::TestCase def test_dup_deep_copy_config - spec = ConnectionSpecification.new({ :a => :b }, "bar") + spec = ConnectionSpecification.new("primary", { :a => :b }, "bar") assert_not_equal(spec.config.object_id, spec.dup.config.object_id) end end diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb index 80244d1439..f2b1d9e4e7 100644 --- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -if current_adapter?(:MysqlAdapter, :Mysql2Adapter) +if current_adapter?(:Mysql2Adapter) module ActiveRecord module ConnectionAdapters class MysqlTypeLookupTest < ActiveRecord::TestCase @@ -22,6 +22,10 @@ module ActiveRecord assert_lookup_type :string, "SET('one', 'two', 'three')" end + def test_set_type_with_value_matching_other_type + assert_lookup_type :string, "SET('unicode', '8bit', 'none', 'time')" + end + def test_enum_type_with_value_matching_other_type assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')" end diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index c7531f5418..db832fe55d 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -29,7 +29,7 @@ module ActiveRecord def test_clearing @cache.columns('posts') @cache.columns_hash('posts') - @cache.tables('posts') + @cache.data_sources('posts') @cache.primary_keys('posts') @cache.clear! @@ -40,17 +40,22 @@ module ActiveRecord def test_dump_and_load @cache.columns('posts') @cache.columns_hash('posts') - @cache.tables('posts') + @cache.data_sources('posts') @cache.primary_keys('posts') @cache = Marshal.load(Marshal.dump(@cache)) assert_equal 11, @cache.columns('posts').size assert_equal 11, @cache.columns_hash('posts').size - assert @cache.tables('posts') + assert @cache.data_sources('posts') assert_equal 'id', @cache.primary_keys('posts') end + def test_table_methods_deprecation + assert_deprecated { assert @cache.table_exists?('posts') } + assert_deprecated { assert @cache.tables('posts') } + assert_deprecated { @cache.clear_table_cache!('posts') } + end end end end diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb index 05c57985a1..3acbafbff4 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strigns for lookup +unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strings for lookup module ActiveRecord module ConnectionAdapters class TypeLookupTest < ActiveRecord::TestCase @@ -81,7 +81,11 @@ module ActiveRecord def test_bigint_limit cast_type = @connection.type_map.lookup("bigint") - assert_equal 8, cast_type.limit + if current_adapter?(:OracleAdapter) + assert_equal 19, cast_type.limit + else + assert_equal 8, cast_type.limit + end end def test_decimal_without_scale diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index f53c496ecd..1f9b6add7a 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -4,6 +4,8 @@ require "rack" module ActiveRecord module ConnectionAdapters class ConnectionManagementTest < ActiveRecord::TestCase + self.use_transactional_tests = false + class App attr_reader :calls def initialize @@ -19,48 +21,20 @@ module ActiveRecord def setup @env = {} @app = App.new - @management = ConnectionManagement.new(@app) + @management = middleware(@app) # make sure we have an active connection assert ActiveRecord::Base.connection assert ActiveRecord::Base.connection_handler.active_connections? end - if Process.respond_to?(:fork) - def test_connection_pool_per_pid - object_id = ActiveRecord::Base.connection.object_id - - rd, wr = IO.pipe - rd.binmode - wr.binmode - - pid = fork { - rd.close - wr.write Marshal.dump ActiveRecord::Base.connection.object_id - wr.close - exit! - } - - wr.close - - Process.waitpid pid - assert_not_equal object_id, Marshal.load(rd.read) - rd.close - end - end - def test_app_delegation - manager = ConnectionManagement.new(@app) + manager = middleware(@app) manager.call @env assert_equal [@env], @app.calls end - def test_connections_are_active_after_call - @management.call(@env) - assert ActiveRecord::Base.connection_handler.active_connections? - end - def test_body_responds_to_each _, _, body = @management.call(@env) bits = [] @@ -74,49 +48,65 @@ module ActiveRecord assert !ActiveRecord::Base.connection_handler.active_connections? end - def test_active_connections_are_not_cleared_on_body_close_during_test - @env['rack.test'] = true - _, _, body = @management.call(@env) - body.close - assert ActiveRecord::Base.connection_handler.active_connections? + def test_active_connections_are_not_cleared_on_body_close_during_transaction + ActiveRecord::Base.transaction do + _, _, body = @management.call(@env) + body.close + assert ActiveRecord::Base.connection_handler.active_connections? + end end def test_connections_closed_if_exception app = Class.new(App) { def call(env); raise NotImplementedError; end }.new - explosive = ConnectionManagement.new(app) + explosive = middleware(app) assert_raises(NotImplementedError) { explosive.call(@env) } assert !ActiveRecord::Base.connection_handler.active_connections? end - def test_connections_not_closed_if_exception_and_test - @env['rack.test'] = true - app = Class.new(App) { def call(env); raise; end }.new - explosive = ConnectionManagement.new(app) - assert_raises(RuntimeError) { explosive.call(@env) } - assert ActiveRecord::Base.connection_handler.active_connections? - end - - def test_connections_closed_if_exception_and_explicitly_not_test - @env['rack.test'] = false - app = Class.new(App) { def call(env); raise NotImplementedError; end }.new - explosive = ConnectionManagement.new(app) - assert_raises(NotImplementedError) { explosive.call(@env) } - assert !ActiveRecord::Base.connection_handler.active_connections? + def test_connections_not_closed_if_exception_inside_transaction + ActiveRecord::Base.transaction do + app = Class.new(App) { def call(env); raise RuntimeError; end }.new + explosive = middleware(app) + assert_raises(RuntimeError) { explosive.call(@env) } + assert ActiveRecord::Base.connection_handler.active_connections? + end end test "doesn't clear active connections when running in a test case" do - @env['rack.test'] = true - @management.call(@env) - assert ActiveRecord::Base.connection_handler.active_connections? + executor.wrap do + @management.call(@env) + assert ActiveRecord::Base.connection_handler.active_connections? + end end - test "proxy is polite to it's body and responds to it" do + test "proxy is polite to its body and responds to it" do body = Class.new(String) { def to_path; "/path"; end }.new app = lambda { |_| [200, {}, body] } - response_body = ConnectionManagement.new(app).call(@env)[2] + response_body = middleware(app).call(@env)[2] assert response_body.respond_to?(:to_path) - assert_equal response_body.to_path, "/path" + assert_equal "/path", response_body.to_path + end + + test "doesn't mutate the original response" do + original_response = [200, {}, 'hi'] + app = lambda { |_| original_response } + middleware(app).call(@env)[2] + assert_equal 'hi', original_response.last end + + private + def executor + @executor ||= Class.new(ActiveSupport::Executor).tap do |exe| + ActiveRecord::QueryCache.install_executor_hooks(exe) + end + end + + def middleware(app) + lambda do |env| + a, b, c = executor.wrap { app.call(env) } + [a, b, Rack::BodyProxy.new(c) { }] + end + end end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 8d15a76735..a45ee281c7 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -1,5 +1,5 @@ require "cases/helper" -require 'active_support/concurrency/latch' +require 'concurrent/atomic/count_down_latch' module ActiveRecord module ConnectionAdapters @@ -100,7 +100,7 @@ module ActiveRecord t = Thread.new { @pool.checkout } # make sure our thread is in the timeout section - Thread.pass until t.status == "sleep" + Thread.pass until @pool.num_waiting_in_queue == 1 connection = cs.first connection.close @@ -112,7 +112,7 @@ module ActiveRecord t = Thread.new { @pool.checkout } # make sure our thread is in the timeout section - Thread.pass until t.status == "sleep" + Thread.pass until @pool.num_waiting_in_queue == 1 connection = cs.first @pool.remove connection @@ -133,15 +133,15 @@ module ActiveRecord end def test_reap_inactive - ready = ActiveSupport::Concurrency::Latch.new + ready = Concurrent::CountDownLatch.new @pool.checkout child = Thread.new do @pool.checkout @pool.checkout - ready.release + ready.count_down Thread.stop end - ready.await + ready.wait assert_equal 3, active_connections(@pool).size @@ -204,13 +204,13 @@ module ActiveRecord end # The connection pool is "fair" if threads waiting for - # connections receive them the order in which they began + # connections receive them in the order in which they began # waiting. This ensures that we don't timeout one HTTP request # even while well under capacity in a multi-threaded environment # such as a Java servlet container. # # We don't need strict fairness: if two connections become - # available at the same time, it's fine of two threads that were + # available at the same time, it's fine if two threads that were # waiting acquire the connections out of order. # # Thus this test prepares waiting threads and then trickles in @@ -234,7 +234,7 @@ module ActiveRecord mutex.synchronize { errors << e } end } - Thread.pass until t.status == "sleep" + Thread.pass until @pool.num_waiting_in_queue == i t end @@ -271,7 +271,7 @@ module ActiveRecord mutex.synchronize { errors << e } end } - Thread.pass until t.status == "sleep" + Thread.pass until @pool.num_waiting_in_queue == i t end @@ -335,11 +335,189 @@ module ActiveRecord # is called with an anonymous class def test_anonymous_class_exception anonymous = Class.new(ActiveRecord::Base) - handler = ActiveRecord::Base.connection_handler - assert_raises(RuntimeError) { - handler.establish_connection anonymous, nil - } + assert_raises(RuntimeError) do + anonymous.establish_connection + end + end + + def test_pool_sets_connection_schema_cache + connection = pool.checkout + schema_cache = SchemaCache.new connection + schema_cache.add(:posts) + pool.schema_cache = schema_cache + + pool.with_connection do |conn| + assert_not_same pool.schema_cache, conn.schema_cache + assert_equal pool.schema_cache.size, conn.schema_cache.size + assert_same pool.schema_cache.columns(:posts), conn.schema_cache.columns(:posts) + end + + pool.checkin connection + end + + def test_concurrent_connection_establishment + assert_operator @pool.connections.size, :<=, 1 + + all_threads_in_new_connection = Concurrent::CountDownLatch.new(@pool.size - @pool.connections.size) + all_go = Concurrent::CountDownLatch.new + + @pool.singleton_class.class_eval do + define_method(:new_connection) do + all_threads_in_new_connection.count_down + all_go.wait + super() + end + end + + connecting_threads = [] + @pool.size.times do + connecting_threads << Thread.new { @pool.checkout } + end + + begin + Timeout.timeout(5) do + # the kernel of the whole test is here, everything else is just scaffolding, + # this latch will not be released unless conn. pool allows for concurrent + # connection creation + all_threads_in_new_connection.wait + end + rescue Timeout::Error + flunk 'pool unable to establish connections concurrently or implementation has ' << + 'changed, this test then needs to patch a different :new_connection method' + ensure + # clean up the threads + all_go.count_down + connecting_threads.map(&:join) + end + end + + def test_non_bang_disconnect_and_clear_reloadable_connections_throw_exception_if_threads_dont_return_their_conns + @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout + [:disconnect, :clear_reloadable_connections].each do |group_action_method| + @pool.with_connection do |connection| + assert_raises(ExclusiveConnectionTimeoutError) do + Thread.new { @pool.send(group_action_method) }.join + end + end + end + end + + def test_disconnect_and_clear_reloadable_connections_attempt_to_wait_for_threads_to_return_their_conns + [:disconnect, :disconnect!, :clear_reloadable_connections, :clear_reloadable_connections!].each do |group_action_method| + begin + thread = timed_join_result = nil + @pool.with_connection do |connection| + thread = Thread.new { @pool.send(group_action_method) } + + # give the other `thread` some time to get stuck in `group_action_method` + timed_join_result = thread.join(0.3) + # thread.join # => `nil` means the other thread hasn't finished running and is still waiting for us to + # release our connection + assert_nil timed_join_result + + # assert that since this is within default timeout our connection hasn't been forcefully taken away from us + assert @pool.active_connection? + end + ensure + thread.join if thread && !timed_join_result # clean up the other thread + end + end + end + + def test_bang_versions_of_disconnect_and_clear_reloadable_connections_if_unable_to_aquire_all_connections_proceed_anyway + @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout + [:disconnect!, :clear_reloadable_connections!].each do |group_action_method| + @pool.with_connection do |connection| + Thread.new { @pool.send(group_action_method) }.join + # assert connection has been forcefully taken away from us + assert_not @pool.active_connection? + end + end + end + + def test_disconnect_and_clear_reloadable_connections_are_able_to_preempt_other_waiting_threads + with_single_connection_pool do |pool| + [:disconnect, :disconnect!, :clear_reloadable_connections, :clear_reloadable_connections!].each do |group_action_method| + conn = pool.connection # drain the only available connection + second_thread_done = Concurrent::CountDownLatch.new + + # create a first_thread and let it get into the FIFO queue first + first_thread = Thread.new do + pool.with_connection { second_thread_done.wait } + end + + # wait for first_thread to get in queue + Thread.pass until pool.num_waiting_in_queue == 1 + + # create a different, later thread, that will attempt to do a "group action", + # but because of the group action semantics it should be able to preempt the + # first_thread when a connection is made available + second_thread = Thread.new do + pool.send(group_action_method) + second_thread_done.count_down + end + + # wait for second_thread to get in queue + Thread.pass until pool.num_waiting_in_queue == 2 + + # return the only available connection + pool.checkin(conn) + + # if the second_thread is not able to preempt the first_thread, + # they will temporarily (until either of them timeouts with ConnectionTimeoutError) + # deadlock and a join(2) timeout will be reached + failed = true unless second_thread.join(2) + + #--- post test clean up start + second_thread_done.count_down if failed + + # after `pool.disconnect()` the first thread will be left stuck in queue, no need to wait for + # it to timeout with ConnectionTimeoutError + if (group_action_method == :disconnect || group_action_method == :disconnect!) && pool.num_waiting_in_queue > 0 + pool.with_connection {} # create a new connection in case there are threads still stuck in a queue + end + + first_thread.join + second_thread.join + #--- post test clean up end + + flunk "#{group_action_method} is not able to preempt other waiting threads" if failed + end + end + end + + def test_clear_reloadable_connections_creates_new_connections_for_waiting_threads_if_necessary + with_single_connection_pool do |pool| + conn = pool.connection # drain the only available connection + def conn.requires_reloading? # make sure it gets removed from the pool by clear_reloadable_connections + true + end + + stuck_thread = Thread.new do + pool.with_connection {} + end + + # wait for stuck_thread to get in queue + Thread.pass until pool.num_waiting_in_queue == 1 + + pool.clear_reloadable_connections + + unless stuck_thread.join(2) + flunk 'clear_reloadable_connections must not let other connection waiting threads get stuck in queue' + end + + assert_equal 0, pool.num_waiting_in_queue + end + end + + private + def with_single_connection_pool + one_conn_spec = ActiveRecord::Base.connection_pool.spec.dup + one_conn_spec.config[:pool] = 1 # this is safe to do, because .dupped ConnectionSpecification also auto-dups its config + yield(pool = ConnectionPool.new(one_conn_spec)) + ensure + pool.disconnect! if pool end end end diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index 3c2f5d4219..3bddaf32ec 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -57,6 +57,12 @@ module ActiveRecord "encoding" => "utf8" }, spec) end + def test_url_missing_scheme + spec = resolve 'foo' + assert_equal({ + "database" => "foo" }, spec) + end + def test_url_host_db spec = resolve 'abstract://foo/bar?encoding=utf8' assert_equal({ @@ -110,6 +116,15 @@ module ActiveRecord "encoding" => "utf8" }, spec) end + def test_spec_name_on_key_lookup + spec = spec(:readonly, 'readonly' => {'adapter' => 'sqlite3'}) + assert_equal "readonly", spec.name + end + + def test_spec_name_with_inline_config + spec = spec({'adapter' => 'sqlite3'}) + assert_equal "primary", spec.name, "should default to primary id" + end end end end diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index 1f5055b2a2..66b4c3f1ff 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -1,6 +1,7 @@ require 'cases/helper' require 'models/topic' require 'models/car' +require 'models/aircraft' require 'models/wheel' require 'models/engine' require 'models/reply' @@ -150,7 +151,7 @@ class CounterCacheTest < ActiveRecord::TestCase test "reset the right counter if two have the same foreign key" do michael = people(:michael) - assert_nothing_raised(ActiveRecord::StatementInvalid) do + assert_nothing_raised do Person.reset_counters(michael.id, :friends_too) end end @@ -198,4 +199,16 @@ class CounterCacheTest < ActiveRecord::TestCase assert_equal 2, car.engines_count assert_equal 2, car.reload.engines_count end + + test "update counters in a polymorphic relationship" do + aircraft = Aircraft.create! + + assert_difference 'aircraft.reload.wheels_count' do + aircraft.wheels << Wheel.create! + end + + assert_difference 'aircraft.reload.wheels_count', -1 do + aircraft.wheels.first.destroy + end + end end diff --git a/activerecord/test/cases/custom_locking_test.rb b/activerecord/test/cases/custom_locking_test.rb index e8290297e3..26d015bf71 100644 --- a/activerecord/test/cases/custom_locking_test.rb +++ b/activerecord/test/cases/custom_locking_test.rb @@ -6,7 +6,7 @@ module ActiveRecord fixtures :people def test_custom_lock - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) assert_match 'SHARE MODE', Person.lock('LOCK IN SHARE MODE').to_sql assert_sql(/LOCK IN SHARE MODE/) do Person.all.merge!(:lock => 'LOCK IN SHARE MODE').find(1) diff --git a/activerecord/test/cases/database_statements_test.rb b/activerecord/test/cases/database_statements_test.rb index c689e97d83..3169408ac0 100644 --- a/activerecord/test/cases/database_statements_test.rb +++ b/activerecord/test/cases/database_statements_test.rb @@ -6,14 +6,29 @@ class DatabaseStatementsTest < ActiveRecord::TestCase end def test_insert_should_return_the_inserted_id + assert_not_nil return_the_inserted_id(method: :insert) + end + + def test_create_should_return_the_inserted_id + assert_not_nil return_the_inserted_id(method: :create) + end + + def test_insert_update_delete_sql_is_deprecated + assert_deprecated { @connection.insert_sql("INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)") } + assert_deprecated { @connection.update_sql("UPDATE accounts SET credit_limit = 6000 WHERE firm_id = 42") } + assert_deprecated { @connection.delete_sql("DELETE FROM accounts WHERE firm_id = 42") } + end + + private + + def return_the_inserted_id(method:) # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method if current_adapter?(:OracleAdapter) sequence_name = "accounts_seq" id_value = @connection.next_sequence_value(sequence_name) - id = @connection.insert("INSERT INTO accounts (id, firm_id,credit_limit) VALUES (accounts_seq.nextval,42,5000)", nil, :id, id_value, sequence_name) + @connection.send(method, "INSERT INTO accounts (id, firm_id,credit_limit) VALUES (accounts_seq.nextval,42,5000)", nil, :id, id_value, sequence_name) else - id = @connection.insert("INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)") + @connection.send(method, "INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)") end - assert_not_nil id end end diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index 698f1b852e..f8664d83bd 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' require 'support/schema_dumping_helper' -if ActiveRecord::Base.connection.supports_datetime_with_precision? +if subsecond_precision_supported? class DateTimePrecisionTest < ActiveRecord::TestCase include SchemaDumpingHelper self.use_transactional_tests = false @@ -10,6 +10,7 @@ class DateTimePrecisionTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection + Foo.reset_column_information end teardown do @@ -20,24 +21,24 @@ class DateTimePrecisionTest < ActiveRecord::TestCase @connection.create_table(:foos, force: true) @connection.add_column :foos, :created_at, :datetime, precision: 0 @connection.add_column :foos, :updated_at, :datetime, precision: 5 - assert_equal 0, activerecord_column_option('foos', 'created_at', 'precision') - assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision') + assert_equal 0, Foo.columns_hash['created_at'].precision + assert_equal 5, Foo.columns_hash['updated_at'].precision end def test_timestamps_helper_with_custom_precision @connection.create_table(:foos, force: true) do |t| t.timestamps precision: 4 end - assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision') - assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision') + assert_equal 4, Foo.columns_hash['created_at'].precision + assert_equal 4, Foo.columns_hash['updated_at'].precision end def test_passing_precision_to_datetime_does_not_set_limit @connection.create_table(:foos, force: true) do |t| t.timestamps precision: 4 end - assert_nil activerecord_column_option('foos', 'created_at', 'limit') - assert_nil activerecord_column_option('foos', 'updated_at', 'limit') + assert_nil Foo.columns_hash['created_at'].limit + assert_nil Foo.columns_hash['updated_at'].limit end def test_invalid_datetime_precision_raises_error @@ -48,14 +49,6 @@ class DateTimePrecisionTest < ActiveRecord::TestCase end end - def test_database_agrees_with_activerecord_about_precision - @connection.create_table(:foos, force: true) do |t| - t.timestamps precision: 4 - end - assert_equal 4, database_datetime_precision('foos', 'created_at') - assert_equal 4, database_datetime_precision('foos', 'updated_at') - end - def test_formatting_datetime_according_to_precision @connection.create_table(:foos, force: true) do |t| t.datetime :created_at, precision: 0 @@ -91,21 +84,5 @@ class DateTimePrecisionTest < ActiveRecord::TestCase end end - private - - def database_datetime_precision(table_name, column_name) - results = @connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'") - result = results.find do |result_hash| - result_hash["column_name"] == column_name - end - result && result["datetime_precision"].to_i - end - - def activerecord_column_option(tablename, column_name, option) - result = @connection.columns(tablename).find do |column| - column.name == column_name - end - result && result.send(option) - end end end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 67fddebf45..067513e24c 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -1,20 +1,14 @@ require "cases/helper" +require 'support/schema_dumping_helper' require 'models/default' require 'models/entrant' class DefaultTest < ActiveRecord::TestCase def test_nil_defaults_for_not_null_columns - column_defaults = - if current_adapter?(:MysqlAdapter) && (Mysql.client_version < 50051 || (50100..50122).include?(Mysql.client_version)) - { 'id' => nil, 'name' => '', 'course_id' => nil } - else - { 'id' => nil, 'name' => nil, 'course_id' => nil } - end - - column_defaults.each do |name, default| + %w(id name course_id).each do |name| column = Entrant.columns_hash[name] assert !column.null, "#{name} column should be NOT NULL" - assert_equal default, column.default, "#{name} column should be DEFAULT #{default.inspect}" + assert_not column.default, "#{name} column should be DEFAULT 'nil'" end end @@ -87,7 +81,32 @@ class DefaultStringsTest < ActiveRecord::TestCase end end -if current_adapter?(:MysqlAdapter, :Mysql2Adapter) +if current_adapter?(:PostgreSQLAdapter) + class PostgresqlDefaultExpressionTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + test "schema dump includes default expression" do + output = dump_table_schema("defaults") + assert_match %r/t\.date\s+"modified_date",\s+default: -> { "\('now'::text\)::date" }/, output + assert_match %r/t\.date\s+"modified_date_function",\s+default: -> { "now\(\)" }/, output + assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "now\(\)" }/, output + assert_match %r/t\.datetime\s+"modified_time_function",\s+default: -> { "now\(\)" }/, output + end + end +end + +if current_adapter?(:Mysql2Adapter) + class MysqlDefaultExpressionTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + if ActiveRecord::Base.connection.version >= '5.6.0' + test "schema dump includes default expression" do + output = dump_table_schema("datetime_defaults") + assert_match %r/t\.datetime\s+"modified_datetime",\s+default: -> { "CURRENT_TIMESTAMP" }/, output + end + end + end + class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase # ActiveRecord::Base#create! (and #save and other related methods) will # open a new transaction. When in transactional tests mode, this will @@ -182,8 +201,7 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) assert_equal '0', klass.columns_hash['zero'].default assert !klass.columns_hash['zero'].null - # 0 in MySQL 4, nil in 5. - assert [0, nil].include?(klass.columns_hash['omit'].default) + assert_equal nil, klass.columns_hash['omit'].default assert !klass.columns_hash['omit'].null assert_raise(ActiveRecord::StatementInvalid) { klass.create! } diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 3a7cc572e6..a3f8d26100 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -37,8 +37,8 @@ class DirtyTest < ActiveRecord::TestCase def test_attribute_changes # New record - no changes. pirate = Pirate.new - assert !pirate.catchphrase_changed? - assert_nil pirate.catchphrase_change + assert_equal false, pirate.catchphrase_changed? + assert_equal false, pirate.non_validated_parrot_id_changed? # Change catchphrase. pirate.catchphrase = 'arrr' @@ -89,7 +89,7 @@ class DirtyTest < ActiveRecord::TestCase target = Class.new(ActiveRecord::Base) target.table_name = 'pirates' - pirate = target.create + pirate = target.create! pirate.created_on = pirate.created_on assert !pirate.created_on_changed? end @@ -467,8 +467,10 @@ class DirtyTest < ActiveRecord::TestCase topic.save! updated_at = topic.updated_at - topic.content[:hello] = 'world' - topic.save! + travel(1.second) do + topic.content[:hello] = 'world' + topic.save! + end assert_not_equal updated_at, topic.updated_at assert_equal 'world', topic.content[:hello] @@ -521,6 +523,9 @@ class DirtyTest < ActiveRecord::TestCase assert_equal Hash.new, pirate.previous_changes pirate = Pirate.find_by_catchphrase("arrr") + + travel(1.second) + pirate.catchphrase = "Me Maties!" pirate.save! @@ -532,6 +537,9 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.previous_changes.key?('created_on') pirate = Pirate.find_by_catchphrase("Me Maties!") + + travel(1.second) + pirate.catchphrase = "Thar She Blows!" pirate.save @@ -542,6 +550,8 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.previous_changes.key?('parrot_id') assert !pirate.previous_changes.key?('created_on') + travel(1.second) + pirate = Pirate.find_by_catchphrase("Thar She Blows!") pirate.update(catchphrase: "Ahoy!") @@ -552,6 +562,8 @@ class DirtyTest < ActiveRecord::TestCase assert !pirate.previous_changes.key?('parrot_id') assert !pirate.previous_changes.key?('created_on') + travel(1.second) + pirate = Pirate.find_by_catchphrase("Ahoy!") pirate.update_attribute(:catchphrase, "Ninjas suck!") @@ -561,6 +573,8 @@ class DirtyTest < ActiveRecord::TestCase assert_not_nil pirate.previous_changes['updated_on'][1] assert !pirate.previous_changes.key?('parrot_id') assert !pirate.previous_changes.key?('created_on') + ensure + travel_back end if ActiveRecord::Base.connection.supports_migrations? @@ -578,6 +592,7 @@ class DirtyTest < ActiveRecord::TestCase end def test_datetime_attribute_can_be_updated_with_fractional_seconds + skip "Fractional seconds are not supported" unless subsecond_precision_supported? in_time_zone 'Paris' do target = Class.new(ActiveRecord::Base) target.table_name = 'topics' @@ -623,32 +638,6 @@ class DirtyTest < ActiveRecord::TestCase end end - test "defaults with type that implements `serialize`" do - type = Class.new(ActiveRecord::Type::Value) do - def cast(value) - value.to_i - end - - def serialize(value) - value.to_s - end - end - - model_class = Class.new(ActiveRecord::Base) do - self.table_name = 'numeric_data' - attribute :foo, type.new, default: 1 - end - - model = model_class.new - assert_not model.foo_changed? - - model = model_class.new(foo: 1) - assert_not model.foo_changed? - - model = model_class.new(foo: '1') - assert_not model.foo_changed? - end - test "in place mutation detection" do pirate = Pirate.create!(catchphrase: "arrrr") pirate.catchphrase << " matey!" @@ -729,6 +718,22 @@ class DirtyTest < ActiveRecord::TestCase assert pirate.catchphrase_changed?(from: "arrrr", to: "arrrr matey!") end + test "getters with side effects are allowed" do + klass = Class.new(Pirate) do + def catchphrase + if super.blank? + update_attribute(:catchphrase, "arr") # what could possibly go wrong? + end + super + end + end + + pirate = klass.create!(catchphrase: "lol") + pirate.update_attribute(:catchphrase, nil) + + assert_equal "arr", pirate.catchphrase + end + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb index 55f0e51717..c25089a420 100644 --- a/activerecord/test/cases/disconnected_test.rb +++ b/activerecord/test/cases/disconnected_test.rb @@ -21,7 +21,9 @@ class TestDisconnectedAdapter < ActiveRecord::TestCase @connection.execute "SELECT count(*) from products" @connection.disconnect! assert_raises(ActiveRecord::StatementInvalid) do - @connection.execute "SELECT count(*) from products" + silence_warnings do + @connection.execute "SELECT count(*) from products" + end end end end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index eea184e530..babacd1ee9 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -9,69 +9,83 @@ class EnumTest < ActiveRecord::TestCase end test "query state by predicate" do - assert @book.proposed? + assert @book.published? assert_not @book.written? - assert_not @book.published? + assert_not @book.proposed? - assert @book.unread? + assert @book.read? + assert @book.in_english? + assert @book.author_visibility_visible? + assert @book.illustrator_visibility_visible? + assert @book.with_medium_font_size? end test "query state with strings" do - assert_equal "proposed", @book.status - assert_equal "unread", @book.read_status + assert_equal "published", @book.status + assert_equal "read", @book.read_status + assert_equal "english", @book.language + assert_equal "visible", @book.author_visibility + assert_equal "visible", @book.illustrator_visibility end test "find via scope" do - assert_equal @book, Book.proposed.first - assert_equal @book, Book.unread.first + assert_equal @book, Book.published.first + assert_equal @book, Book.read.first + assert_equal @book, Book.in_english.first + assert_equal @book, Book.author_visibility_visible.first + assert_equal @book, Book.illustrator_visibility_visible.first end test "find via where with values" do - proposed, written = Book.statuses[:proposed], Book.statuses[:written] + published, written = Book.statuses[:published], Book.statuses[:written] - assert_equal @book, Book.where(status: proposed).first - refute_equal @book, Book.where(status: written).first - assert_equal @book, Book.where(status: [proposed]).first - refute_equal @book, Book.where(status: [written]).first - refute_equal @book, Book.where("status <> ?", proposed).first + assert_equal @book, Book.where(status: published).first + assert_not_equal @book, Book.where(status: written).first + assert_equal @book, Book.where(status: [published]).first + assert_not_equal @book, Book.where(status: [written]).first + assert_not_equal @book, Book.where("status <> ?", published).first assert_equal @book, Book.where("status <> ?", written).first end test "find via where with symbols" do - assert_equal @book, Book.where(status: :proposed).first - refute_equal @book, Book.where(status: :written).first - assert_equal @book, Book.where(status: [:proposed]).first - refute_equal @book, Book.where(status: [:written]).first - refute_equal @book, Book.where.not(status: :proposed).first + assert_equal @book, Book.where(status: :published).first + assert_not_equal @book, Book.where(status: :written).first + assert_equal @book, Book.where(status: [:published]).first + assert_not_equal @book, Book.where(status: [:written]).first + assert_not_equal @book, Book.where.not(status: :published).first assert_equal @book, Book.where.not(status: :written).first end test "find via where with strings" do - assert_equal @book, Book.where(status: "proposed").first - refute_equal @book, Book.where(status: "written").first - assert_equal @book, Book.where(status: ["proposed"]).first - refute_equal @book, Book.where(status: ["written"]).first - refute_equal @book, Book.where.not(status: "proposed").first + assert_equal @book, Book.where(status: "published").first + assert_not_equal @book, Book.where(status: "written").first + assert_equal @book, Book.where(status: ["published"]).first + assert_not_equal @book, Book.where(status: ["written"]).first + assert_not_equal @book, Book.where.not(status: "published").first assert_equal @book, Book.where.not(status: "written").first end test "build from scope" do assert Book.written.build.written? - refute Book.written.build.proposed? + assert_not Book.written.build.proposed? end test "build from where" do assert Book.where(status: Book.statuses[:written]).build.written? - refute Book.where(status: Book.statuses[:written]).build.proposed? + assert_not Book.where(status: Book.statuses[:written]).build.proposed? assert Book.where(status: :written).build.written? - refute Book.where(status: :written).build.proposed? + assert_not Book.where(status: :written).build.proposed? assert Book.where(status: "written").build.written? - refute Book.where(status: "written").build.proposed? + assert_not Book.where(status: "written").build.proposed? end test "update by declaration" do @book.written! assert @book.written? + @book.in_english! + assert @book.in_english? + @book.author_visibility_visible! + assert @book.author_visibility_visible? end test "update by setter" do @@ -96,42 +110,61 @@ class EnumTest < ActiveRecord::TestCase test "enum changed attributes" do old_status = @book.status - @book.status = :published + old_language = @book.language + @book.status = :proposed + @book.language = :spanish assert_equal old_status, @book.changed_attributes[:status] + assert_equal old_language, @book.changed_attributes[:language] end test "enum changes" do old_status = @book.status - @book.status = :published - assert_equal [old_status, 'published'], @book.changes[:status] + old_language = @book.language + @book.status = :proposed + @book.language = :spanish + assert_equal [old_status, 'proposed'], @book.changes[:status] + assert_equal [old_language, 'spanish'], @book.changes[:language] end test "enum attribute was" do old_status = @book.status + old_language = @book.language @book.status = :published + @book.language = :spanish assert_equal old_status, @book.attribute_was(:status) + assert_equal old_language, @book.attribute_was(:language) end test "enum attribute changed" do - @book.status = :published + @book.status = :proposed + @book.language = :french assert @book.attribute_changed?(:status) + assert @book.attribute_changed?(:language) end test "enum attribute changed to" do - @book.status = :published - assert @book.attribute_changed?(:status, to: 'published') + @book.status = :proposed + @book.language = :french + assert @book.attribute_changed?(:status, to: 'proposed') + assert @book.attribute_changed?(:language, to: 'french') end test "enum attribute changed from" do old_status = @book.status - @book.status = :published + old_language = @book.language + @book.status = :proposed + @book.language = :french assert @book.attribute_changed?(:status, from: old_status) + assert @book.attribute_changed?(:language, from: old_language) end test "enum attribute changed from old status to new status" do old_status = @book.status - @book.status = :published - assert @book.attribute_changed?(:status, from: old_status, to: 'published') + old_language = @book.language + @book.status = :proposed + @book.language = :french + assert @book.attribute_changed?(:status, from: old_status, to: 'proposed') + assert @book.attribute_changed?(:language, from: old_language, to: 'french') end test "enum didn't change" do @@ -141,7 +174,7 @@ class EnumTest < ActiveRecord::TestCase end test "persist changes that are dirty" do - @book.status = :published + @book.status = :proposed assert @book.attribute_changed?(:status) @book.status = :written assert @book.attribute_changed?(:status) @@ -149,7 +182,7 @@ class EnumTest < ActiveRecord::TestCase test "reverted changes that are not dirty" do old_status = @book.status - @book.status = :published + @book.status = :proposed assert @book.attribute_changed?(:status) @book.status = old_status assert_not @book.attribute_changed?(:status) @@ -201,18 +234,22 @@ class EnumTest < ActiveRecord::TestCase test "building new objects with enum scopes" do assert Book.written.build.written? assert Book.read.build.read? + assert Book.in_spanish.build.in_spanish? + assert Book.illustrator_visibility_invisible.build.illustrator_visibility_invisible? end test "creating new objects with enum scopes" do assert Book.written.create.written? assert Book.read.create.read? + assert Book.in_spanish.create.in_spanish? + assert Book.illustrator_visibility_invisible.create.illustrator_visibility_invisible? end test "_before_type_cast returns the enum label (required for form fields)" do if @book.status_came_from_user? - assert_equal "proposed", @book.status_before_type_cast + assert_equal "published", @book.status_before_type_cast else - assert_equal "proposed", @book.status + assert_equal "published", @book.status end end @@ -355,4 +392,33 @@ class EnumTest < ActiveRecord::TestCase book2 = klass.single.create! assert book2.single? end + + test "query state by predicate with prefix" do + assert @book.author_visibility_visible? + assert_not @book.author_visibility_invisible? + assert @book.illustrator_visibility_visible? + assert_not @book.illustrator_visibility_invisible? + end + + test "query state by predicate with custom prefix" do + assert @book.in_english? + assert_not @book.in_spanish? + assert_not @book.in_french? + end + + test "uses default status when no status is provided in fixtures" do + book = books(:tlg) + assert book.proposed?, "expected fixture to default to proposed status" + assert book.in_english?, "expected fixture to default to english language" + end + + test "uses default value from database on initialization" do + book = Book.new + assert book.proposed? + end + + test "uses default value from database on initialization when using custom mapping" do + book = Book.new + assert book.hard? + end end diff --git a/activerecord/test/cases/errors_test.rb b/activerecord/test/cases/errors_test.rb new file mode 100644 index 0000000000..0711a372f2 --- /dev/null +++ b/activerecord/test/cases/errors_test.rb @@ -0,0 +1,16 @@ +require_relative "../cases/helper" + +class ErrorsTest < ActiveRecord::TestCase + def test_can_be_instantiated_with_no_args + base = ActiveRecord::ActiveRecordError + error_klasses = ObjectSpace.each_object(Class).select { |klass| klass < base } + + error_klasses.each do |error_klass| + begin + error_klass.new.inspect + rescue ArgumentError + raise "Instance of #{error_klass} can't be initialized with no arguments" + end + end + end +end diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index 8de2ddb10d..2dee8a26a5 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -48,6 +48,11 @@ if ActiveRecord::Base.connection.supports_explain? assert queries.empty? end + def test_collects_cte_queries + SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'with s as (values(3)) select 1 from s') + assert_equal 1, queries.size + end + teardown do ActiveRecord::ExplainRegistry.reset end diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index f1d5511bb8..64dfd86ce2 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -39,38 +39,49 @@ if ActiveRecord::Base.connection.supports_explain? binds = [[], []] queries = sqls.zip(binds) - connection.stubs(:explain).returns('query plan foo', 'query plan bar') - expected = sqls.map {|sql| "EXPLAIN for: #{sql}\nquery plan #{sql}"}.join("\n") - assert_equal expected, base.exec_explain(queries) + stub_explain_for_query_plans do + expected = sqls.map {|sql| "EXPLAIN for: #{sql}\nquery plan #{sql}"}.join("\n") + assert_equal expected, base.exec_explain(queries) + end end def test_exec_explain_with_binds - cols = [Object.new, Object.new] - cols[0].expects(:name).returns('wadus') - cols[1].expects(:name).returns('chaflan') + object = Struct.new(:name) + cols = [object.new('wadus'), object.new('chaflan')] sqls = %w(foo bar) binds = [[[cols[0], 1]], [[cols[1], 2]]] queries = sqls.zip(binds) - connection.stubs(:explain).returns("query plan foo\n", "query plan bar\n") - expected = <<-SQL.strip_heredoc - EXPLAIN for: #{sqls[0]} [["wadus", 1]] - query plan foo + stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do + expected = <<-SQL.strip_heredoc + EXPLAIN for: #{sqls[0]} [["wadus", 1]] + query plan foo - EXPLAIN for: #{sqls[1]} [["chaflan", 2]] - query plan bar - SQL - assert_equal expected, base.exec_explain(queries) + EXPLAIN for: #{sqls[1]} [["chaflan", 2]] + query plan bar + SQL + assert_equal expected, base.exec_explain(queries) + end end def test_unsupported_connection_adapter - connection.stubs(:supports_explain?).returns(false) + connection.stub(:supports_explain?, false) do + assert_not_called(base.logger, :warn) do + Car.where(:name => 'honda').to_a + end + end + end - base.logger.expects(:warn).never + private - Car.where(:name => 'honda').to_a - end + def stub_explain_for_query_plans(query_plans = ['query plan foo', 'query plan bar']) + explain_called = 0 + + connection.stub(:explain, proc{ explain_called += 1; query_plans[explain_called - 1] }) do + yield + end + end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 39308866ee..374a8ba199 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -19,7 +19,7 @@ require 'models/car' require 'models/tyre' class FinderTest < ActiveRecord::TestCase - fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations, :cars + fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :author_addresses, :customers, :categories, :categorizations, :cars def test_find_by_id_with_hash assert_raises(ActiveRecord::StatementInvalid) do @@ -43,11 +43,80 @@ class FinderTest < ActiveRecord::TestCase end assert_equal "should happen", exception.message - assert_nothing_raised(RuntimeError) do + assert_nothing_raised do Topic.all.find(-> { raise "should not happen" }) { |e| e.title == topics(:first).title } end end + def test_find_with_ids_returning_ordered + records = Topic.find([4,2,5]) + assert_equal 'The Fourth Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + + records = Topic.find(4,2,5) + assert_equal 'The Fourth Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + + records = Topic.find(['4','2','5']) + assert_equal 'The Fourth Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + + records = Topic.find('4','2','5') + assert_equal 'The Fourth Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + end + + def test_find_with_ids_and_order_clause + # The order clause takes precedence over the informed ids + records = Topic.order(:author_name).find([5,3,1]) + assert_equal 'The Third Topic of the day', records[0].title + assert_equal 'The First Topic', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + + records = Topic.order(:id).find([5,3,1]) + assert_equal 'The First Topic', records[0].title + assert_equal 'The Third Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + end + + def test_find_with_ids_with_limit_and_order_clause + # The order clause takes precedence over the informed ids + records = Topic.limit(2).order(:id).find([5,3,1]) + assert_equal 2, records.size + assert_equal 'The First Topic', records[0].title + assert_equal 'The Third Topic of the day', records[1].title + end + + def test_find_with_ids_and_limit + records = Topic.limit(3).find([3,2,5,1,4]) + assert_equal 3, records.size + assert_equal 'The Third Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + end + + def test_find_with_ids_where_and_limit + # Please note that Topic 1 is the only not approved so + # if it were among the first 3 it would raise an ActiveRecord::RecordNotFound + records = Topic.where(approved: true).limit(3).find([3,2,5,1,4]) + assert_equal 3, records.size + assert_equal 'The Third Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + end + + def test_find_with_ids_and_offset + records = Topic.offset(2).find([3,2,5,1,4]) + assert_equal 3, records.size + assert_equal 'The Fifth Topic of the day', records[0].title + assert_equal 'The First Topic', records[1].title + assert_equal 'The Fourth Topic of the day', records[2].title + end + def test_find_passing_active_record_object_is_deprecated assert_deprecated do Topic.find(Topic.last) @@ -105,7 +174,7 @@ class FinderTest < ActiveRecord::TestCase end def test_exists_fails_when_parameter_has_invalid_type - assert_raises(RangeError) do + assert_raises(ActiveModel::RangeError) do assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int end assert_equal false, Topic.exists?("foo") @@ -178,8 +247,9 @@ class FinderTest < ActiveRecord::TestCase end def test_exists_does_not_instantiate_records - Developer.expects(:instantiate).never - Developer.exists? + assert_not_called(Developer, :instantiate) do + Developer.exists? + end end def test_find_by_array_of_one_id @@ -194,7 +264,9 @@ class FinderTest < ActiveRecord::TestCase def test_find_by_ids_with_limit_and_offset assert_equal 2, Entrant.limit(2).find([1,3,2]).size - assert_equal 1, Entrant.limit(3).offset(2).find([1,3,2]).size + entrants = Entrant.limit(3).offset(2).find([1,3,2]) + assert_equal 1, entrants.size + assert_equal 'Ruby Guru', entrants.first.name # Also test an edge case: If you have 11 results, and you set a # limit of 3 and offset of 9, then you should find that there @@ -202,6 +274,8 @@ class FinderTest < ActiveRecord::TestCase devs = Developer.all last_devs = Developer.limit(3).offset(9).find devs.map(&:id) assert_equal 2, last_devs.size + assert_equal 'fixture_10', last_devs[0].name + assert_equal 'Jamis', last_devs[1].name end def test_find_with_large_number @@ -264,6 +338,12 @@ class FinderTest < ActiveRecord::TestCase assert_equal [Account], accounts.collect(&:class).uniq end + def test_find_by_association_subquery + author = authors(:david) + assert_equal author.post, Post.find_by(author: Author.where(id: author)) + assert_equal author.post, Post.find_by(author_id: Author.where(id: author)) + end + def test_take assert_equal topics(:first), Topic.take end @@ -406,6 +486,66 @@ class FinderTest < ActiveRecord::TestCase end end + def test_second_to_last + assert_equal topics(:fourth).title, Topic.second_to_last.title + + # test with offset + assert_equal topics(:fourth), Topic.offset(1).second_to_last + assert_equal topics(:fourth), Topic.offset(2).second_to_last + assert_equal topics(:fourth), Topic.offset(3).second_to_last + assert_equal nil, Topic.offset(4).second_to_last + assert_equal nil, Topic.offset(5).second_to_last + + #test with limit + # assert_equal nil, Topic.limit(1).second # TODO: currently failing + assert_equal nil, Topic.limit(1).second_to_last + end + + def test_second_to_last_have_primary_key_order_by_default + expected = topics(:fourth) + expected.touch # PostgreSQL changes the default order if no order clause is used + assert_equal expected, Topic.second_to_last + end + + def test_model_class_responds_to_second_to_last_bang + assert Topic.second_to_last! + Topic.delete_all + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do + Topic.second_to_last! + end + end + + def test_third_to_last + assert_equal topics(:third).title, Topic.third_to_last.title + + # test with offset + assert_equal topics(:third), Topic.offset(1).third_to_last + assert_equal topics(:third), Topic.offset(2).third_to_last + assert_equal nil, Topic.offset(3).third_to_last + assert_equal nil, Topic.offset(4).third_to_last + assert_equal nil, Topic.offset(5).third_to_last + + # test with limit + # assert_equal nil, Topic.limit(1).third # TODO: currently failing + assert_equal nil, Topic.limit(1).third_to_last + # assert_equal nil, Topic.limit(2).third # TODO: currently failing + assert_equal nil, Topic.limit(2).third_to_last + end + + def test_third_to_last_have_primary_key_order_by_default + expected = topics(:third) + expected.touch # PostgreSQL changes the default order if no order clause is used + assert_equal expected, Topic.third_to_last + end + + def test_model_class_responds_to_third_to_last_bang + assert Topic.third_to_last! + Topic.delete_all + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do + Topic.third_to_last! + end + end + def test_last_bang_present assert_nothing_raised do assert_equal topics(:second), Topic.where("title = 'The Second Topic of the day'").last! @@ -427,25 +567,53 @@ class FinderTest < ActiveRecord::TestCase end def test_take_and_first_and_last_with_integer_should_use_sql_limit - assert_sql(/LIMIT 3|ROWNUM <= 3/) { Topic.take(3).entries } - assert_sql(/LIMIT 2|ROWNUM <= 2/) { Topic.first(2).entries } - assert_sql(/LIMIT 5|ROWNUM <= 5/) { Topic.last(5).entries } + assert_sql(/LIMIT|ROWNUM <=/) { Topic.take(3).entries } + assert_sql(/LIMIT|ROWNUM <=/) { Topic.first(2).entries } + assert_sql(/LIMIT|ROWNUM <=/) { Topic.last(5).entries } end def test_last_with_integer_and_order_should_keep_the_order assert_equal Topic.order("title").to_a.last(2), Topic.order("title").last(2) end - def test_last_with_integer_and_order_should_not_use_sql_limit - query = assert_sql { Topic.order("title").last(5).entries } - assert_equal 1, query.length - assert_no_match(/LIMIT/, query.first) + def test_last_with_integer_and_order_should_use_sql_limit + relation = Topic.order("title") + assert_queries(1) { relation.last(5) } + assert !relation.loaded? + end + + def test_last_with_integer_and_reorder_should_use_sql_limit + relation = Topic.reorder("title") + assert_queries(1) { relation.last(5) } + assert !relation.loaded? + end + + def test_last_on_loaded_relation_should_not_use_sql + relation = Topic.limit(10).load + assert_no_queries do + relation.last + relation.last(2) + end + end + + def test_last_with_irreversible_order + assert_deprecated do + Topic.order("coalesce(author_name, title)").last + end end - def test_last_with_integer_and_reorder_should_not_use_sql_limit - query = assert_sql { Topic.reorder("title").last(5).entries } - assert_equal 1, query.length - assert_no_match(/LIMIT/, query.first) + def test_last_on_relation_with_limit_and_offset + post = posts('sti_comments') + + comments = post.comments.order(id: :asc) + assert_equal comments.limit(2).to_a.last, comments.limit(2).last + assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2) + assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3) + + comments = comments.offset(1) + assert_equal comments.limit(2).to_a.last, comments.limit(2).last + assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2) + assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3) end def test_take_and_first_and_last_with_integer_should_return_an_array @@ -484,11 +652,16 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { Topic.where(approved: true).find(1) } end - def test_find_on_hash_conditions_with_explicit_table_name + def test_find_on_hash_conditions_with_qualified_attribute_dot_notation_string assert Topic.where('topics.approved' => false).find(1) assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => true).find(1) } end + def test_find_on_hash_conditions_with_qualified_attribute_dot_notation_symbol + assert Topic.where('topics.approved': false).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved': true).find(1) } + end + def test_find_on_hash_conditions_with_hashed_table_name assert Topic.where(topics: { approved: false }).find(1) assert_raise(ActiveRecord::RecordNotFound) { Topic.where(topics: { approved: true }).find(1) } @@ -699,90 +872,13 @@ class FinderTest < ActiveRecord::TestCase assert Company.where(["name = :name", {name: "37signals' go'es agains"}]).first end - def test_bind_arity - assert_nothing_raised { bind '' } - assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '', 1 } - - assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?' } - assert_nothing_raised { bind '?', 1 } - assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 } - end - def test_named_bind_variables - assert_equal '1', bind(':a', :a => 1) # ' ruby-mode - assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode - - assert_nothing_raised { bind("'+00:00'", :foo => "bar") } - assert_kind_of Firm, Company.where(["name = :name", { name: "37signals" }]).first assert_nil Company.where(["name = :name", { name: "37signals!" }]).first assert_nil Company.where(["name = :name", { name: "37signals!' OR 1=1" }]).first assert_kind_of Time, Topic.where(["id = :id", { id: 1 }]).first.written_on end - class SimpleEnumerable - include Enumerable - - def initialize(ary) - @ary = ary - end - - def each(&b) - @ary.each(&b) - end - end - - def test_bind_enumerable - quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')}) - - assert_equal '1,2,3', bind('?', [1, 2, 3]) - assert_equal quoted_abc, bind('?', %w(a b c)) - - assert_equal '1,2,3', bind(':a', :a => [1, 2, 3]) - assert_equal quoted_abc, bind(':a', :a => %w(a b c)) # ' - - assert_equal '1,2,3', bind('?', SimpleEnumerable.new([1, 2, 3])) - assert_equal quoted_abc, bind('?', SimpleEnumerable.new(%w(a b c))) - - assert_equal '1,2,3', bind(':a', :a => SimpleEnumerable.new([1, 2, 3])) - assert_equal quoted_abc, bind(':a', :a => SimpleEnumerable.new(%w(a b c))) # ' - end - - def test_bind_empty_enumerable - quoted_nil = ActiveRecord::Base.connection.quote(nil) - assert_equal quoted_nil, bind('?', []) - assert_equal " in (#{quoted_nil})", bind(' in (?)', []) - assert_equal "foo in (#{quoted_nil})", bind('foo in (?)', []) - end - - def test_bind_empty_string - quoted_empty = ActiveRecord::Base.connection.quote('') - assert_equal quoted_empty, bind('?', '') - end - - def test_bind_chars - quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") - quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") - assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi") - assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper") - assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi".mb_chars) - assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper".mb_chars) - end - - def test_bind_record - o = Struct.new(:quoted_id).new(1) - assert_equal '1', bind('?', o) - - os = [o] * 3 - assert_equal '1,1,1', bind('?', os) - end - - def test_named_bind_with_postgresql_type_casts - l = Proc.new { bind(":a::integer '2009-01-01'::date", :a => '10') } - assert_nothing_raised(&l) - assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call - end - def test_string_sanitation assert_not_equal "'something ' 1=1'", ActiveRecord::Base.sanitize("something ' 1=1") assert_equal "'something; select table'", ActiveRecord::Base.sanitize("something; select table") @@ -947,7 +1043,6 @@ class FinderTest < ActiveRecord::TestCase end end - # http://dev.rubyonrails.org/ticket/6778 def test_find_ignores_previously_inserted_record Post.create!(:title => 'test', :body => 'it out') assert_equal [], Post.where(id: nil) @@ -991,10 +1086,13 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_order_on_included_associations_with_construct_finder_sql_for_association_limiting_and_is_distinct - assert_equal 2, Post.includes(authors: :author_address).order('author_addresses.id DESC ').limit(2).to_a.size + assert_equal 2, Post.includes(authors: :author_address). + where.not(author_addresses: { id: nil }). + order('author_addresses.id DESC').limit(2).to_a.size assert_equal 3, Post.includes(author: :author_address, authors: :author_address). - order('author_addresses_authors.id DESC ').limit(3).to_a.size + where.not(author_addresses_authors: { id: nil }). + order('author_addresses_authors.id DESC').limit(3).to_a.size end def test_find_with_nil_inside_set_passed_for_one_attribute @@ -1053,7 +1151,7 @@ class FinderTest < ActiveRecord::TestCase end def test_finder_with_offset_string - assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.offset("3").to_a } + assert_nothing_raised { Topic.offset("3").to_a } end test "find_by with hash conditions returns the first matching record" do @@ -1124,14 +1222,6 @@ class FinderTest < ActiveRecord::TestCase end protected - def bind(statement, *vars) - if vars.first.is_a?(Hash) - ActiveRecord::Base.send(:replace_named_bind_variables, statement, vars.first) - else - ActiveRecord::Base.send(:replace_bind_variables, statement, vars) - end - end - def table_with_custom_primary_key yield(Class.new(Toy) do def self.name diff --git a/activerecord/test/cases/fixture_set/file_test.rb b/activerecord/test/cases/fixture_set/file_test.rb index 92efa8aca7..e64b90507e 100644 --- a/activerecord/test/cases/fixture_set/file_test.rb +++ b/activerecord/test/cases/fixture_set/file_test.rb @@ -123,6 +123,24 @@ END end end + def test_removes_fixture_config_row + File.open(::File.join(FIXTURES_ROOT, 'other_posts.yml')) do |fh| + assert_equal(['second_welcome'], fh.each.map { |name, _| name }) + end + end + + def test_extracts_model_class_from_config_row + File.open(::File.join(FIXTURES_ROOT, 'other_posts.yml')) do |fh| + assert_equal 'Post', fh.model_class + end + end + + def test_erb_filename + filename = 'filename.yaml' + erb = File.new(filename).send(:prepare_erb, "<% Rails.env %>\n") + assert_equal erb.filename, filename + end + private def tmp_yaml(name, contents) t = Tempfile.new name diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index f8acdcb51e..da934ab8fe 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -7,11 +7,12 @@ require 'models/binary' require 'models/book' require 'models/bulb' require 'models/category' +require 'models/comment' require 'models/company' require 'models/computer' require 'models/course' require 'models/developer' -require 'models/computer' +require 'models/doubloon' require 'models/joke' require 'models/matey' require 'models/parrot' @@ -183,7 +184,6 @@ class FixturesTest < ActiveRecord::TestCase end def test_fixtures_from_root_yml_with_instantiation - # assert_equal 2, @accounts.size assert_equal 50, @unknown.credit_limit end @@ -216,6 +216,17 @@ class FixturesTest < ActiveRecord::TestCase end end + def test_yaml_file_with_invalid_column + e = assert_raise(ActiveRecord::Fixture::FixtureError) do + ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "parrots") + end + assert_equal(%(table "parrots" has no column named "arrr".), e.message) + end + + def test_yaml_file_with_symbol_columns + ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "trees") + end + def test_omap_fixtures assert_nothing_raised do fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', Category, FIXTURES_ROOT + "/categories_ordered") @@ -251,18 +262,19 @@ class FixturesTest < ActiveRecord::TestCase def test_fixtures_are_set_up_with_database_env_variable db_url_tmp = ENV['DATABASE_URL'] ENV['DATABASE_URL'] = "sqlite3::memory:" - ActiveRecord::Base.stubs(:configurations).returns({}) - test_case = Class.new(ActiveRecord::TestCase) do - fixtures :accounts + ActiveRecord::Base.stub(:configurations, {}) do + test_case = Class.new(ActiveRecord::TestCase) do + fixtures :accounts - def test_fixtures - assert accounts(:signals37) + def test_fixtures + assert accounts(:signals37) + end end - end - result = test_case.new(:test_fixtures).run + result = test_case.new(:test_fixtures).run - assert result.passed?, "Expected #{result.name} to pass:\n#{result}" + assert result.passed?, "Expected #{result.name} to pass:\n#{result}" + end ensure ENV['DATABASE_URL'] = db_url_tmp end @@ -279,10 +291,10 @@ class HasManyThroughFixture < ActiveSupport::TestCase treasure = make_model "Treasure" pt.table_name = "parrots_treasures" - pt.belongs_to :parrot, :class => parrot - pt.belongs_to :treasure, :class => treasure + pt.belongs_to :parrot, :anonymous_class => parrot + pt.belongs_to :treasure, :anonymous_class => treasure - parrot.has_many :parrot_treasures, :class => pt + parrot.has_many :parrot_treasures, :anonymous_class => pt parrot.has_many :treasures, :through => :parrot_treasures parrots = File.join FIXTURES_ROOT, 'parrots' @@ -297,10 +309,10 @@ class HasManyThroughFixture < ActiveSupport::TestCase parrot = make_model "Parrot" treasure = make_model "Treasure" - pt.belongs_to :parrot, :class => parrot - pt.belongs_to :treasure, :class => treasure + pt.belongs_to :parrot, :anonymous_class => parrot + pt.belongs_to :treasure, :anonymous_class => treasure - parrot.has_many :parrot_treasures, :class => pt + parrot.has_many :parrot_treasures, :anonymous_class => pt parrot.has_many :treasures, :through => :parrot_treasures parrots = File.join FIXTURES_ROOT, 'parrots' @@ -400,9 +412,11 @@ class FixturesWithoutInstantiationTest < ActiveRecord::TestCase end def test_reloading_fixtures_through_accessor_methods + topic = Struct.new(:title) assert_equal "The First Topic", topics(:first).title - @loaded_fixtures['topics']['first'].expects(:find).returns(stub(:title => "Fresh Topic!")) - assert_equal "Fresh Topic!", topics(:first, true).title + assert_called(@loaded_fixtures['topics']['first'], :find, returns: topic.new("Fresh Topic!")) do + assert_equal "Fresh Topic!", topics(:first, true).title + end end end @@ -506,6 +520,38 @@ class OverRideFixtureMethodTest < ActiveRecord::TestCase end end +class FixtureWithSetModelClassTest < ActiveRecord::TestCase + fixtures :other_posts, :other_comments + + # Set to false to blow away fixtures cache and ensure our fixtures are loaded + # and thus takes into account the +set_model_class+. + self.use_transactional_tests = false + + def test_uses_fixture_class_defined_in_yaml + assert_kind_of Post, other_posts(:second_welcome) + end + + def test_loads_the_associations_to_fixtures_with_set_model_class + post = other_posts(:second_welcome) + comment = other_comments(:second_greetings) + assert_equal [comment], post.comments + assert_equal post, comment.post + end +end + +class SetFixtureClassPrevailsTest < ActiveRecord::TestCase + set_fixture_class bad_posts: Post + fixtures :bad_posts + + # Set to false to blow away fixtures cache and ensure our fixtures are loaded + # and thus takes into account the +set_model_class+. + self.use_transactional_tests = false + + def test_uses_set_fixture_class + assert_kind_of Post, bad_posts(:bad_welcome) + end +end + class CheckSetTableNameFixturesTest < ActiveRecord::TestCase set_fixture_class :funny_jokes => Joke fixtures :funny_jokes @@ -691,7 +737,7 @@ end class FoxyFixturesTest < ActiveRecord::TestCase fixtures :parrots, :parrots_pirates, :pirates, :treasures, :mateys, :ships, :computers, - :developers, :"admin/accounts", :"admin/users", :live_parrots, :dead_parrots + :developers, :"admin/accounts", :"admin/users", :live_parrots, :dead_parrots, :books if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' require 'models/uuid_parent' @@ -841,6 +887,13 @@ class FoxyFixturesTest < ActiveRecord::TestCase assert admin_accounts(:signals37).users.include?(admin_users(:david)) assert_equal 2, admin_accounts(:signals37).users.size end + + def test_resolves_enums + assert books(:awdr).published? + assert books(:awdr).read? + assert books(:rfr).proposed? + assert books(:ddd).published? + end end class ActiveSupportSubclassWithFixturesTest < ActiveRecord::TestCase @@ -896,3 +949,26 @@ class FixturesWithDefaultScopeTest < ActiveRecord::TestCase assert_equal "special", bulbs(:special).name end end + +class FixturesWithAbstractBelongsTo < ActiveRecord::TestCase + fixtures :pirates, :doubloons + + test "creates fixtures with belongs_to associations defined in abstract base classes" do + assert_not_nil doubloons(:blackbeards_doubloon) + assert_equal pirates(:blackbeard), doubloons(:blackbeards_doubloon).pirate + end +end + +class FixtureClassNamesTest < ActiveRecord::TestCase + def setup + @saved_cache = self.fixture_class_names.dup + end + + def teardown + self.fixture_class_names.replace(@saved_cache) + end + + test "fixture_class_names returns nil for unregistered identifier" do + assert_nil self.fixture_class_names['unregistered_identifier'] + end +end diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb index f4e7646f03..91921469b8 100644 --- a/activerecord/test/cases/forbidden_attributes_protection_test.rb +++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb @@ -1,14 +1,20 @@ require 'cases/helper' require 'active_support/core_ext/hash/indifferent_access' -require 'models/person' + require 'models/company' +require 'models/person' +require 'models/ship' +require 'models/ship_part' +require 'models/treasure' -class ProtectedParams < ActiveSupport::HashWithIndifferentAccess +class ProtectedParams attr_accessor :permitted alias :permitted? :permitted + delegate :keys, :key?, :has_key?, :empty?, to: :@parameters + def initialize(attributes) - super(attributes) + @parameters = attributes.with_indifferent_access @permitted = false end @@ -17,6 +23,18 @@ class ProtectedParams < ActiveSupport::HashWithIndifferentAccess self end + def [](key) + @parameters[key] + end + + def to_h + @parameters + end + + def stringify_keys + dup + end + def dup super.tap do |duplicate| duplicate.instance_variable_set :@permitted, @permitted @@ -75,6 +93,13 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase end end + def test_create_with_works_with_permitted_params + params = ProtectedParams.new(first_name: 'Guille').permit! + + person = Person.create_with(params).create! + assert_equal 'Guille', person.first_name + end + def test_create_with_works_with_params_values params = ProtectedParams.new(first_name: 'Guille') @@ -90,10 +115,51 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase end end + def test_where_works_with_permitted_params + params = ProtectedParams.new(first_name: 'Guille').permit! + + person = Person.where(params).create! + assert_equal 'Guille', person.first_name + end + def test_where_works_with_params_values params = ProtectedParams.new(first_name: 'Guille') person = Person.where(first_name: params[:first_name]).create! assert_equal 'Guille', person.first_name end + + def test_where_not_checks_permitted + params = ProtectedParams.new(first_name: 'Guille', gender: 'm') + + assert_raises(ActiveModel::ForbiddenAttributesError) do + Person.where().not(params) + end + end + + def test_where_not_works_with_permitted_params + params = ProtectedParams.new(first_name: 'Guille').permit! + Person.create!(params) + assert_empty Person.where.not(params).select {|p| p.first_name == 'Guille' } + end + + def test_strong_params_style_objects_work_with_singular_associations + params = ProtectedParams.new( name: "Stern", ship_attributes: ProtectedParams.new(name: "The Black Rock").permit!).permit! + part = ShipPart.new(params) + + assert_equal "Stern", part.name + assert_equal "The Black Rock", part.ship.name + end + + def test_strong_params_style_objects_work_with_collection_associations + params = ProtectedParams.new( + trinkets_attributes: ProtectedParams.new( + "0" => ProtectedParams.new(name: "Necklace").permit!, + "1" => ProtectedParams.new(name: "Spoon").permit! ) ).permit! + part = ShipPart.new(params) + + assert_equal "Necklace", part.trinkets[0].name + assert_equal "Spoon", part.trinkets[1].name + end + end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 12c793c408..d2fdf03e9d 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -1,8 +1,7 @@ -require File.expand_path('../../../../load_paths', __FILE__) - require 'config' require 'active_support/testing/autorun' +require 'active_support/testing/method_call_assertions' require 'stringio' require 'active_record' @@ -45,13 +44,12 @@ def in_memory_db? ActiveRecord::Base.connection_pool.spec.config[:database] == ":memory:" end -def mysql_56? - current_adapter?(:MysqlAdapter, :Mysql2Adapter) && - ActiveRecord::Base.connection.send(:version).join(".") >= "5.6.0" +def subsecond_precision_supported? + ActiveRecord::Base.connection.supports_datetime_with_precision? end def mysql_enforcing_gtid_consistency? - current_adapter?(:MysqlAdapter, :Mysql2Adapter) && 'ON' == ActiveRecord::Base.connection.show_variable('enforce_gtid_consistency') + current_adapter?(:Mysql2Adapter) && 'ON' == ActiveRecord::Base.connection.show_variable('enforce_gtid_consistency') end def supports_savepoints? @@ -140,6 +138,7 @@ require "cases/validations_repair_helper" class ActiveSupport::TestCase include ActiveRecord::TestFixtures include ActiveRecord::ValidationsRepairHelper + include ActiveSupport::Testing::MethodCallAssertions self.fixture_path = FIXTURES_ROOT self.use_instantiated_fixtures = false diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb index 5ba9a1029a..9fc75b7377 100644 --- a/activerecord/test/cases/hot_compatibility_test.rb +++ b/activerecord/test/cases/hot_compatibility_test.rb @@ -1,7 +1,9 @@ require 'cases/helper' +require 'support/connection_helper' class HotCompatibilityTest < ActiveRecord::TestCase self.use_transactional_tests = false + include ConnectionHelper setup do @klass = Class.new(ActiveRecord::Base) do @@ -51,4 +53,90 @@ class HotCompatibilityTest < ActiveRecord::TestCase record.reload assert_equal 'bar', record.foo end + + if current_adapter?(:PostgreSQLAdapter) + test "cleans up after prepared statement failure in a transaction" do + with_two_connections do |original_connection, ddl_connection| + record = @klass.create! bar: 'bar' + + # prepare the reload statement in a transaction + @klass.transaction do + record.reload + end + + assert get_prepared_statement_cache(@klass.connection).any?, + "expected prepared statement cache to have something in it" + + # add a new column + ddl_connection.add_column :hot_compatibilities, :baz, :string + + assert_raise(ActiveRecord::PreparedStatementCacheExpired) do + @klass.transaction do + record.reload + end + end + + assert_empty get_prepared_statement_cache(@klass.connection), + "expected prepared statement cache to be empty but it wasn't" + end + end + + test "cleans up after prepared statement failure in nested transactions" do + with_two_connections do |original_connection, ddl_connection| + record = @klass.create! bar: 'bar' + + # prepare the reload statement in a transaction + @klass.transaction do + record.reload + end + + assert get_prepared_statement_cache(@klass.connection).any?, + "expected prepared statement cache to have something in it" + + # add a new column + ddl_connection.add_column :hot_compatibilities, :baz, :string + + assert_raise(ActiveRecord::PreparedStatementCacheExpired) do + @klass.transaction do + @klass.transaction do + @klass.transaction do + record.reload + end + end + end + end + + assert_empty get_prepared_statement_cache(@klass.connection), + "expected prepared statement cache to be empty but it wasn't" + end + end + end + + private + + def get_prepared_statement_cache(connection) + connection.instance_variable_get(:@statements) + .instance_variable_get(:@cache)[Process.pid] + end + + # Rails will automatically clear the prepared statements on the connection + # that runs the migration, so we use two connections to simulate what would + # actually happen on a production system; we'd have one connection running the + # migration from the rake task ("ddl_connection" here), and we'd have another + # connection in a web worker. + def with_two_connections + run_without_connection do |original_connection| + ActiveRecord::Base.establish_connection(original_connection.merge(pool_size: 2)) + begin + ddl_connection = ActiveRecord::Base.connection_pool.checkout + begin + yield original_connection, ddl_connection + ensure + ActiveRecord::Base.connection_pool.checkin ddl_connection + end + ensure + ActiveRecord::Base.clear_all_connections! + end + end + end end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 3268555cb8..e234b9a6a9 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -1,4 +1,5 @@ require 'cases/helper' +require 'models/author' require 'models/company' require 'models/person' require 'models/post' @@ -6,16 +7,34 @@ require 'models/project' require 'models/subscriber' require 'models/vegetables' require 'models/shop' +require 'models/sponsor' + +module InheritanceTestHelper + def with_store_full_sti_class(&block) + assign_store_full_sti_class true, &block + end + + def without_store_full_sti_class(&block) + assign_store_full_sti_class false, &block + end + + def assign_store_full_sti_class(flag) + old_store_full_sti_class = ActiveRecord::Base.store_full_sti_class + ActiveRecord::Base.store_full_sti_class = flag + yield + ensure + ActiveRecord::Base.store_full_sti_class = old_store_full_sti_class + end +end class InheritanceTest < ActiveRecord::TestCase + include InheritanceTestHelper fixtures :companies, :projects, :subscribers, :accounts, :vegetables def test_class_with_store_full_sti_class_returns_full_name - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = true - assert_equal 'Namespaced::Company', Namespaced::Company.sti_name - ensure - ActiveRecord::Base.store_full_sti_class = old + with_store_full_sti_class do + assert_equal 'Namespaced::Company', Namespaced::Company.sti_name + end end def test_class_with_blank_sti_name @@ -33,39 +52,104 @@ class InheritanceTest < ActiveRecord::TestCase end def test_class_without_store_full_sti_class_returns_demodulized_name - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = false - assert_equal 'Company', Namespaced::Company.sti_name - ensure - ActiveRecord::Base.store_full_sti_class = old + without_store_full_sti_class do + assert_equal 'Company', Namespaced::Company.sti_name + end + end + + def test_compute_type_success + assert_equal Author, ActiveRecord::Base.send(:compute_type, 'Author') + end + + def test_compute_type_nonexistent_constant + e = assert_raises NameError do + ActiveRecord::Base.send :compute_type, 'NonexistentModel' + end + assert_equal 'uninitialized constant ActiveRecord::Base::NonexistentModel', e.message + assert_equal 'ActiveRecord::Base::NonexistentModel', e.name + end + + def test_compute_type_no_method_error + ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise NoMethodError }) do + assert_raises NoMethodError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end + end + end + + def test_compute_type_on_undefined_method + error = nil + begin + Class.new(Author) do + alias_method :foo, :bar + end + rescue => e + error = e + end + + ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise e }) do + + exception = assert_raises NameError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end + assert_equal error.message, exception.message + end + end + + def test_compute_type_argument_error + ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise ArgumentError }) do + assert_raises ArgumentError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end + end end def test_should_store_demodulized_class_name_with_store_full_sti_class_option_disabled - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = false - item = Namespaced::Company.new - assert_equal 'Company', item[:type] - ensure - ActiveRecord::Base.store_full_sti_class = old + without_store_full_sti_class do + item = Namespaced::Company.new + assert_equal 'Company', item[:type] + end end def test_should_store_full_class_name_with_store_full_sti_class_option_enabled - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = true - item = Namespaced::Company.new - assert_equal 'Namespaced::Company', item[:type] - ensure - ActiveRecord::Base.store_full_sti_class = old + with_store_full_sti_class do + item = Namespaced::Company.new + assert_equal 'Namespaced::Company', item[:type] + end end def test_different_namespace_subclass_should_load_correctly_with_store_full_sti_class_option - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = true - item = Namespaced::Company.create :name => "Wolverine 2" - assert_not_nil Company.find(item.id) - assert_not_nil Namespaced::Company.find(item.id) - ensure - ActiveRecord::Base.store_full_sti_class = old + with_store_full_sti_class do + item = Namespaced::Company.create name: "Wolverine 2" + assert_not_nil Company.find(item.id) + assert_not_nil Namespaced::Company.find(item.id) + end + end + + def test_descends_from_active_record + assert !ActiveRecord::Base.descends_from_active_record? + + # Abstract subclass of AR::Base. + assert LoosePerson.descends_from_active_record? + + # Concrete subclass of an abstract class. + assert LooseDescendant.descends_from_active_record? + + # Concrete subclass of AR::Base. + assert TightPerson.descends_from_active_record? + + # Concrete subclass of a concrete class but has no type column. + assert TightDescendant.descends_from_active_record? + + # Concrete subclass of AR::Base. + assert Post.descends_from_active_record? + + # Abstract subclass of a concrete class which has a type column. + # This is pathological, as you'll never have Sub < Abstract < Concrete. + assert !StiPost.descends_from_active_record? + + # Concrete subclasses an abstract class which has a type column. + assert !SubStiPost.descends_from_active_record? end def test_company_descends_from_active_record @@ -75,6 +159,12 @@ class InheritanceTest < ActiveRecord::TestCase assert !Class.new(Company).descends_from_active_record?, 'Company subclass should not descend from ActiveRecord::Base' end + def test_abstract_class + assert !ActiveRecord::Base.abstract_class? + assert LoosePerson.abstract_class? + assert !LooseDescendant.abstract_class? + end + def test_inheritance_base_class assert_equal Post, Post.base_class assert_equal Post, SpecialPost.base_class @@ -204,10 +294,27 @@ class InheritanceTest < ActiveRecord::TestCase assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'Account') } end + def test_new_with_unrelated_namespaced_type + without_store_full_sti_class do + e = assert_raises ActiveRecord::SubclassNotFound do + Namespaced::Company.new(type: 'Firm') + end + + assert_equal "Invalid single-table inheritance type: Namespaced::Firm is not a subclass of Namespaced::Company", e.message + end + end + def test_new_with_complex_inheritance assert_nothing_raised { Client.new(type: 'VerySpecialClient') } end + def test_new_without_storing_full_sti_class + without_store_full_sti_class do + item = Company.new(type: 'SpecialCo') + assert_instance_of Company::SpecialCo, item + end + end + def test_new_with_autoload_paths path = File.expand_path('../../models/autoloadable', __FILE__) ActiveSupport::Dependencies.autoload_paths << path @@ -331,40 +438,35 @@ class InheritanceTest < ActiveRecord::TestCase end class InheritanceComputeTypeTest < ActiveRecord::TestCase + include InheritanceTestHelper fixtures :companies - def setup - ActiveSupport::Dependencies.log_activity = true - end - teardown do - ActiveSupport::Dependencies.log_activity = false self.class.const_remove :FirmOnTheFly rescue nil Firm.const_remove :FirmOnTheFly rescue nil end def test_instantiation_doesnt_try_to_require_corresponding_file - ActiveRecord::Base.store_full_sti_class = false - foo = Firm.first.clone - foo.type = 'FirmOnTheFly' - foo.save! + without_store_full_sti_class do + foo = Firm.first.clone + foo.type = 'FirmOnTheFly' + foo.save! - # Should fail without FirmOnTheFly in the type condition. - assert_raise(ActiveRecord::RecordNotFound) { Firm.find(foo.id) } + # Should fail without FirmOnTheFly in the type condition. + assert_raise(ActiveRecord::RecordNotFound) { Firm.find(foo.id) } - # Nest FirmOnTheFly in the test case where Dependencies won't see it. - self.class.const_set :FirmOnTheFly, Class.new(Firm) - assert_raise(ActiveRecord::SubclassNotFound) { Firm.find(foo.id) } + # Nest FirmOnTheFly in the test case where Dependencies won't see it. + self.class.const_set :FirmOnTheFly, Class.new(Firm) + assert_raise(ActiveRecord::SubclassNotFound) { Firm.find(foo.id) } - # Nest FirmOnTheFly in Firm where Dependencies will see it. - # This is analogous to nesting models in a migration. - Firm.const_set :FirmOnTheFly, Class.new(Firm) + # Nest FirmOnTheFly in Firm where Dependencies will see it. + # This is analogous to nesting models in a migration. + Firm.const_set :FirmOnTheFly, Class.new(Firm) - # And instantiate will find the existing constant rather than trying - # to require firm_on_the_fly. - assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find(foo.id) } - ensure - ActiveRecord::Base.store_full_sti_class = true + # And instantiate will find the existing constant rather than trying + # to require firm_on_the_fly. + assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find(foo.id) } + end end def test_sti_type_from_attributes_disabled_in_non_sti_class @@ -372,4 +474,128 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase product = Shop::Product.new(:type => phone) assert product.save end + + def test_inheritance_new_with_subclass_as_default + original_type = Company.columns_hash["type"].default + ActiveRecord::Base.connection.change_column_default :companies, :type, 'Firm' + Company.reset_column_information + + firm = Company.new # without arguments + assert_equal 'Firm', firm.type + assert_instance_of Firm, firm + + firm = Company.new(firm_name: 'Shri Hans Plastic') # with arguments + assert_equal 'Firm', firm.type + assert_instance_of Firm, firm + + client = Client.new + assert_equal 'Client', client.type + assert_instance_of Client, client + + firm = Company.new(type: 'Client') # overwrite the default type + assert_equal 'Client', firm.type + assert_instance_of Client, firm + ensure + ActiveRecord::Base.connection.change_column_default :companies, :type, original_type + Company.reset_column_information + end +end + +class InheritanceAttributeTest < ActiveRecord::TestCase + + class Company < ActiveRecord::Base + self.table_name = 'companies' + attribute :type, :string, default: "InheritanceAttributeTest::Startup" + end + + class Startup < Company + end + + class Empire < Company + end + + def test_inheritance_new_with_subclass_as_default + startup = Company.new # without arguments + assert_equal 'InheritanceAttributeTest::Startup', startup.type + assert_instance_of Startup, startup + + empire = Company.new(type: 'InheritanceAttributeTest::Empire') # without arguments + assert_equal 'InheritanceAttributeTest::Empire', empire.type + assert_instance_of Empire, empire + end +end + +class InheritanceAttributeMappingTest < ActiveRecord::TestCase + setup do + @old_registry = ActiveRecord::Type.registry + ActiveRecord::Type.registry = ActiveRecord::Type::AdapterSpecificRegistry.new + ActiveRecord::Type.register :omg_sti, InheritanceAttributeMappingTest::OmgStiType + Company.delete_all + Sponsor.delete_all + end + + teardown do + ActiveRecord::Type.registry = @old_registry + end + + class OmgStiType < ActiveRecord::Type::String + def cast_value(value) + if value =~ /\Aomg_(.+)\z/ + $1.classify + else + value + end + end + + def serialize(value) + if value + "omg_%s" % value.underscore + end + end + end + + class Company < ActiveRecord::Base + self.table_name = 'companies' + attribute :type, :omg_sti + end + + class Startup < Company; end + class Empire < Company; end + + class Sponsor < ActiveRecord::Base + self.table_name = 'sponsors' + attribute :sponsorable_type, :omg_sti + + belongs_to :sponsorable, polymorphic: true + end + + def test_sti_with_custom_type + Startup.create! name: 'a Startup' + Empire.create! name: 'an Empire' + + assert_equal [["a Startup", "omg_inheritance_attribute_mapping_test/startup"], + ["an Empire", "omg_inheritance_attribute_mapping_test/empire"]], ActiveRecord::Base.connection.select_rows('SELECT name, type FROM companies').sort + assert_equal [["a Startup", "InheritanceAttributeMappingTest::Startup"], + ["an Empire", "InheritanceAttributeMappingTest::Empire"]], Company.all.map { |a| [a.name, a.type] }.sort + + startup = Startup.first + startup.becomes! Empire + startup.save! + + assert_equal [["a Startup", "omg_inheritance_attribute_mapping_test/empire"], + ["an Empire", "omg_inheritance_attribute_mapping_test/empire"]], ActiveRecord::Base.connection.select_rows('SELECT name, type FROM companies').sort + + assert_equal [["a Startup", "InheritanceAttributeMappingTest::Empire"], + ["an Empire", "InheritanceAttributeMappingTest::Empire"]], Company.all.map { |a| [a.name, a.type] }.sort + end + + def test_polymorphic_associations_custom_type + startup = Startup.create! name: 'a Startup' + sponsor = Sponsor.create! sponsorable: startup + + assert_equal ["omg_inheritance_attribute_mapping_test/company"], ActiveRecord::Base.connection.select_values('SELECT sponsorable_type FROM sponsors') + + sponsor = Sponsor.first + assert_equal startup, sponsor.sponsorable + end end diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 018b7b0d8f..08a186ae07 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -81,7 +81,7 @@ class IntegrationTest < ActiveRecord::TestCase def test_cache_key_format_for_existing_record_with_updated_at dev = Developer.first - assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key + assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:usec)}", dev.cache_key end def test_cache_key_format_for_existing_record_with_updated_at_and_custom_cache_timestamp_format @@ -96,7 +96,9 @@ class IntegrationTest < ActiveRecord::TestCase owner.update_column :updated_at, Time.current key = owner.cache_key - assert pet.touch + travel(1.second) do + assert pet.touch + end assert_not_equal key, owner.reload.cache_key end @@ -109,30 +111,39 @@ class IntegrationTest < ActiveRecord::TestCase def test_cache_key_for_updated_on dev = Developer.first dev.updated_at = nil - assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key + assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:usec)}", dev.cache_key end def test_cache_key_for_newer_updated_at dev = Developer.first dev.updated_at += 3600 - assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key + assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:usec)}", dev.cache_key end def test_cache_key_for_newer_updated_on dev = Developer.first dev.updated_on += 3600 - assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key + assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:usec)}", dev.cache_key end def test_cache_key_format_is_precise_enough + skip("Subsecond precision is not supported") unless subsecond_precision_supported? dev = Developer.first key = dev.cache_key dev.touch assert_not_equal key, dev.cache_key end + def test_cache_key_format_is_not_too_precise + skip("Subsecond precision is not supported") unless subsecond_precision_supported? + dev = Developer.first + dev.touch + key = dev.cache_key + assert_equal key, dev.reload.cache_key + end + def test_named_timestamps_for_cache_key owner = owners(:blackbeard) - assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:nsec)}", owner.cache_key(:updated_at, :happy_at) + assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at) end end diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb index 6523fc29fd..a16b52751a 100644 --- a/activerecord/test/cases/invalid_connection_test.rb +++ b/activerecord/test/cases/invalid_connection_test.rb @@ -1,5 +1,6 @@ require "cases/helper" +if current_adapter?(:Mysql2Adapter) class TestAdapterWithInvalidConnection < ActiveRecord::TestCase self.use_transactional_tests = false @@ -9,7 +10,7 @@ class TestAdapterWithInvalidConnection < ActiveRecord::TestCase def setup # Can't just use current adapter; sqlite3 will create a database # file on the fly. - Bird.establish_connection adapter: 'mysql', database: 'i_do_not_exist' + Bird.establish_connection adapter: 'mysql2', database: 'i_do_not_exist' end teardown do @@ -20,3 +21,4 @@ class TestAdapterWithInvalidConnection < ActiveRecord::TestCase assert_equal "#{Bird.name} (call '#{Bird.name}.connection' to establish a connection)", Bird.inspect end end +end diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 8144f3e5c5..e030f6c588 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -1,8 +1,11 @@ require "cases/helper" +class Horse < ActiveRecord::Base +end + module ActiveRecord class InvertibleMigrationTest < ActiveRecord::TestCase - class SilentMigration < ActiveRecord::Migration + class SilentMigration < ActiveRecord::Migration::Current def write(text = '') # sssshhhhh!! end @@ -76,7 +79,33 @@ module ActiveRecord end end - class LegacyMigration < ActiveRecord::Migration + class ChangeColumnDefault1 < SilentMigration + def change + create_table("horses") do |t| + t.column :name, :string, default: "Sekitoba" + end + end + end + + class ChangeColumnDefault2 < SilentMigration + def change + change_column_default :horses, :name, from: "Sekitoba", to: "Diomed" + end + end + + class DisableExtension1 < SilentMigration + def change + enable_extension "hstore" + end + end + + class DisableExtension2 < SilentMigration + def change + disable_extension "hstore" + end + end + + class LegacyMigration < ActiveRecord::Migration::Current def self.up create_table("horses") do |t| t.column :content, :text @@ -128,8 +157,10 @@ module ActiveRecord teardown do %w[horses new_horses].each do |table| - if ActiveRecord::Base.connection.table_exists?(table) - ActiveRecord::Base.connection.drop_table(table) + ActiveSupport::Deprecation.silence do + if ActiveRecord::Base.connection.table_exists?(table) + ActiveRecord::Base.connection.drop_table(table) + end end end ActiveRecord::Migration.verbose = @verbose_was @@ -144,26 +175,30 @@ module ActiveRecord end def test_exception_on_removing_index_without_column_option - RemoveIndexMigration1.new.migrate(:up) - migration = RemoveIndexMigration2.new - migration.migrate(:up) + index_definition = ["horses", [:name, :color]] + migration1 = RemoveIndexMigration1.new + migration1.migrate(:up) + assert migration1.connection.index_exists?(*index_definition) - assert_raises(IrreversibleMigration) do - migration.migrate(:down) - end + migration2 = RemoveIndexMigration2.new + migration2.migrate(:up) + assert_not migration2.connection.index_exists?(*index_definition) + + migration2.migrate(:down) + assert migration2.connection.index_exists?(*index_definition) end def test_migrate_up migration = InvertibleMigration.new migration.migrate(:up) - assert migration.connection.table_exists?("horses"), "horses should exist" + ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses"), "horses should exist" } end def test_migrate_down migration = InvertibleMigration.new migration.migrate :up migration.migrate :down - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } end def test_migrate_revert @@ -171,11 +206,11 @@ module ActiveRecord revert = InvertibleRevertMigration.new migration.migrate :up revert.migrate :up - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } revert.migrate :down - assert migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") } migration.migrate :down - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } end def test_migrate_revert_by_part @@ -183,18 +218,24 @@ module ActiveRecord received = [] migration = InvertibleByPartsMigration.new migration.test = ->(dir){ - assert migration.connection.table_exists?("horses") - assert migration.connection.table_exists?("new_horses") + ActiveSupport::Deprecation.silence do + assert migration.connection.table_exists?("horses") + assert migration.connection.table_exists?("new_horses") + end received << dir } migration.migrate :up assert_equal [:both, :up], received - assert !migration.connection.table_exists?("horses") - assert migration.connection.table_exists?("new_horses") + ActiveSupport::Deprecation.silence do + assert !migration.connection.table_exists?("horses") + assert migration.connection.table_exists?("new_horses") + end migration.migrate :down assert_equal [:both, :up, :both, :down], received - assert migration.connection.table_exists?("horses") - assert !migration.connection.table_exists?("new_horses") + ActiveSupport::Deprecation.silence do + assert migration.connection.table_exists?("horses") + assert !migration.connection.table_exists?("new_horses") + end end def test_migrate_revert_whole_migration @@ -203,20 +244,56 @@ module ActiveRecord revert = RevertWholeMigration.new(klass) migration.migrate :up revert.migrate :up - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } revert.migrate :down - assert migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") } migration.migrate :down - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } end end def test_migrate_nested_revert_whole_migration revert = NestedRevertWholeMigration.new(InvertibleRevertMigration) revert.migrate :down - assert revert.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert revert.connection.table_exists?("horses") } revert.migrate :up - assert !revert.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !revert.connection.table_exists?("horses") } + end + + def test_migrate_revert_change_column_default + migration1 = ChangeColumnDefault1.new + migration1.migrate(:up) + assert_equal "Sekitoba", Horse.new.name + + migration2 = ChangeColumnDefault2.new + migration2.migrate(:up) + Horse.reset_column_information + assert_equal "Diomed", Horse.new.name + + migration2.migrate(:down) + Horse.reset_column_information + assert_equal "Sekitoba", Horse.new.name + end + + if current_adapter?(:PostgreSQLAdapter) + def test_migrate_enable_and_disable_extension + migration1 = InvertibleMigration.new + migration2 = DisableExtension1.new + migration3 = DisableExtension2.new + + migration1.migrate(:up) + migration2.migrate(:up) + assert_equal true, Horse.connection.extension_enabled?('hstore') + + migration3.migrate(:up) + assert_equal false, Horse.connection.extension_enabled?('hstore') + + migration3.migrate(:down) + assert_equal true, Horse.connection.extension_enabled?('hstore') + + migration2.migrate(:down) + assert_equal false, Horse.connection.extension_enabled?('hstore') + end end def test_revert_order @@ -245,24 +322,24 @@ module ActiveRecord def test_legacy_up LegacyMigration.migrate :up - assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" + ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" } end def test_legacy_down LegacyMigration.migrate :up LegacyMigration.migrate :down - assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" + ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" } end def test_up LegacyMigration.up - assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" + ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" } end def test_down LegacyMigration.up LegacyMigration.down - assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" + ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" } end def test_migrate_down_with_table_name_prefix @@ -271,13 +348,13 @@ module ActiveRecord migration = InvertibleMigration.new migration.migrate(:up) assert_nothing_raised { migration.migrate(:down) } - assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" + ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" } ensure ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = '' end # MySQL 5.7 and Oracle do not allow to create duplicate indexes on the same columns - unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :OracleAdapter) + unless current_adapter?(:Mysql2Adapter, :OracleAdapter) def test_migrate_revert_add_index_with_name RevertNamedIndexMigration1.new.migrate(:up) RevertNamedIndexMigration2.new.migrate(:up) diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 9e4998a946..9fc0041892 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -169,6 +169,12 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 1, p1.lock_version end + def test_lock_new_when_explicitly_passing_nil + p1 = Person.new(:first_name => 'anika', lock_version: nil) + p1.save! + assert_equal 0, p1.lock_version + end + def test_touch_existing_lock p1 = Person.find(1) assert_equal 0, p1.lock_version @@ -177,6 +183,16 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 1, p1.lock_version end + def test_touch_stale_object + person = Person.create!(first_name: 'Mehmet Emin') + stale_person = Person.find(person.id) + person.update_attribute(:gender, 'M') + + assert_raises(ActiveRecord::StaleObjectError) do + stale_person.touch + end + end + def test_lock_column_name_existing t1 = LegacyThing.find(1) t2 = LegacyThing.find(1) @@ -260,7 +276,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase car.wheels << Wheel.create! end assert_difference 'car.wheels.count', -1 do - car.destroy + car.reload.destroy end assert car.destroyed? end @@ -431,7 +447,7 @@ unless in_memory_db? def test_lock_sending_custom_lock_statement Person.transaction do person = Person.find(1) - assert_sql(/LIMIT 1 FOR SHARE NOWAIT/) do + assert_sql(/LIMIT \$?\d FOR SHARE NOWAIT/) do person.lock!('FOR SHARE NOWAIT') end end diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 4192d12ff4..707a2d1da1 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -7,6 +7,20 @@ require "active_support/log_subscriber/test_helper" class LogSubscriberTest < ActiveRecord::TestCase include ActiveSupport::LogSubscriber::TestHelper include ActiveSupport::Logger::Severity + REGEXP_CLEAR = Regexp.escape(ActiveRecord::LogSubscriber::CLEAR) + REGEXP_BOLD = Regexp.escape(ActiveRecord::LogSubscriber::BOLD) + REGEXP_MAGENTA = Regexp.escape(ActiveRecord::LogSubscriber::MAGENTA) + REGEXP_CYAN = Regexp.escape(ActiveRecord::LogSubscriber::CYAN) + SQL_COLORINGS = { + SELECT: Regexp.escape(ActiveRecord::LogSubscriber::BLUE), + INSERT: Regexp.escape(ActiveRecord::LogSubscriber::GREEN), + UPDATE: Regexp.escape(ActiveRecord::LogSubscriber::YELLOW), + DELETE: Regexp.escape(ActiveRecord::LogSubscriber::RED), + LOCK: Regexp.escape(ActiveRecord::LogSubscriber::WHITE), + ROLLBACK: Regexp.escape(ActiveRecord::LogSubscriber::RED), + TRANSACTION: REGEXP_CYAN, + OTHER: REGEXP_MAGENTA + } class TestDebugLogSubscriber < ActiveRecord::LogSubscriber attr_reader :debugs @@ -71,6 +85,90 @@ class LogSubscriberTest < ActiveRecord::TestCase assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last) end + def test_basic_query_logging_coloration + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.each do |verb, color_regex| + logger.sql(event.new(0, sql: verb.to_s)) + assert_match(/#{REGEXP_BOLD}#{color_regex}#{verb}#{REGEXP_CLEAR}/i, logger.debugs.last) + end + end + + def test_basic_payload_name_logging_coloration_generic_sql + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.each do |verb, _| + logger.sql(event.new(0, sql: verb.to_s)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + + logger.sql(event.new(0, {sql: verb.to_s, name: "SQL"})) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + end + end + + def test_basic_payload_name_logging_coloration_named_sql + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.each do |verb, _| + logger.sql(event.new(0, {sql: verb.to_s, name: "Model Load"})) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + + logger.sql(event.new(0, {sql: verb.to_s, name: "Model Exists"})) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + + logger.sql(event.new(0, {sql: verb.to_s, name: "ANY SPECIFIC NAME"})) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + end + end + + def test_query_logging_coloration_with_nested_select + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex| + logger.sql(event.new(0, sql: "#{verb} WHERE ID IN SELECT")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last) + end + end + + def test_query_logging_coloration_with_multi_line_nested_select + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex| + sql = <<-EOS + #{verb} + WHERE ID IN ( + SELECT ID FROM THINGS + ) + EOS + logger.sql(event.new(0, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + end + end + + def test_query_logging_coloration_with_lock + event = Struct.new(:duration, :payload) + logger = TestDebugLogSubscriber.new + logger.colorize_logging = true + sql = <<-EOS + SELECT * FROM + (SELECT * FROM mytable FOR UPDATE) ss + WHERE col1 = 5; + EOS + logger.sql(event.new(0, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + + sql = <<-EOS + LOCK TABLE films IN SHARE MODE; + EOS + logger.sql(event.new(0, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + end + def test_exists_query_logging Developer.exists? 1 wait @@ -111,7 +209,7 @@ class LogSubscriberTest < ActiveRecord::TestCase Thread.new { assert_equal 0, ActiveRecord::LogSubscriber.runtime }.join end - unless current_adapter?(:Mysql2Adapter) + if ActiveRecord::Base.connection.prepared_statements def test_binary_data_is_not_logged Binary.create(data: 'some binary data') wait diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 46a62c272f..d6963b48d7 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -50,7 +50,7 @@ module ActiveRecord def test_create_table_with_defaults # MySQL doesn't allow defaults on TEXT or BLOB columns. - mysql = current_adapter?(:MysqlAdapter, :Mysql2Adapter) + mysql = current_adapter?(:Mysql2Adapter) connection.create_table :testings do |t| t.column :one, :string, :default => "hello" @@ -105,7 +105,7 @@ module ActiveRecord eight = columns.detect { |c| c.name == "eight_int" } if current_adapter?(:OracleAdapter) - assert_equal 'NUMBER(8)', eight.sql_type + assert_equal 'NUMBER(19)', eight.sql_type elsif current_adapter?(:SQLite3Adapter) assert_equal 'bigint', eight.sql_type else @@ -141,7 +141,7 @@ module ActiveRecord assert_equal 'smallint', one.sql_type assert_equal 'integer', four.sql_type assert_equal 'bigint', eight.sql_type - elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter) + elsif current_adapter?(:Mysql2Adapter) assert_match 'int(11)', default.sql_type assert_match 'tinyint', one.sql_type assert_match 'int', four.sql_type @@ -339,7 +339,7 @@ module ActiveRecord def test_change_column_null testing_table_with_only_foo_attribute do - notnull_migration = Class.new(ActiveRecord::Migration) do + notnull_migration = Class.new(ActiveRecord::Migration::Current) do def change change_column_null :testings, :foo, false end @@ -405,9 +405,9 @@ module ActiveRecord def test_drop_table_if_exists connection.create_table(:testings) - assert connection.table_exists?(:testings) + ActiveSupport::Deprecation.silence { assert connection.table_exists?(:testings) } connection.drop_table(:testings, if_exists: true) - assert_not connection.table_exists?(:testings) + ActiveSupport::Deprecation.silence { assert_not connection.table_exists?(:testings) } end def test_drop_table_if_exists_nothing_raised @@ -442,7 +442,7 @@ module ActiveRecord end def test_create_table_with_force_cascade_drops_dependent_objects - skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" if current_adapter?(:Mysql2Adapter) # can't re-create table referenced by foreign key assert_raises(ActiveRecord::StatementInvalid) do @connection.create_table :trains, force: true diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index 2ffe7a1b0d..2f9c50141f 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -1,5 +1,4 @@ require "cases/migration/helper" -require "minitest/mock" module ActiveRecord class Migration diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 8d8e661aa5..c7a1b81a75 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -37,13 +37,13 @@ module ActiveRecord def test_add_column_without_limit # TODO: limit: nil should work with all adapters. - skip "MySQL wrongly enforces a limit of 255" if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + skip "MySQL wrongly enforces a limit of 255" if current_adapter?(:Mysql2Adapter) add_column :test_models, :description, :string, limit: nil TestModel.reset_column_information assert_nil TestModel.columns_hash["description"].limit end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_unabstracted_database_dependent_types add_column :test_models, :intelligence_quotient, :tinyint TestModel.reset_column_information @@ -63,8 +63,6 @@ module ActiveRecord # Do a manual insertion if current_adapter?(:OracleAdapter) connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)" - elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before MySQL 5.0.3 decimals stored as strings - connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')" elsif current_adapter?(:PostgreSQLAdapter) connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" else @@ -171,7 +169,7 @@ module ActiveRecord end end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) def test_out_of_range_limit_should_raise assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 } diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb index 4637970ce0..8294da0373 100644 --- a/activerecord/test/cases/migration/column_positioning_test.rb +++ b/activerecord/test/cases/migration/column_positioning_test.rb @@ -23,7 +23,7 @@ module ActiveRecord ActiveRecord::Base.primary_key_prefix_type = nil end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_column_positioning assert_equal %w(first second third), conn.columns(:testings).map(&:name) end diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 5fc7702dfa..fca1cb7e97 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -62,7 +62,7 @@ module ActiveRecord assert_equal '70000', default_after end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_mysql_rename_column_preserves_auto_increment rename_column "test_models", "id", "id_test" assert connection.columns("test_models").find { |c| c.name == "id_test" }.auto_increment? @@ -267,6 +267,13 @@ module ActiveRecord assert_nil TestModel.new.first_name end + def test_change_column_default_with_from_and_to + add_column "test_models", "first_name", :string + connection.change_column_default "test_models", "first_name", from: nil, to: "Tester" + + assert_equal "Tester", TestModel.new.first_name + end + def test_remove_column_no_second_parameter_raises_exception assert_raise(ArgumentError) { connection.remove_column("funny") } end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 3844b1a92e..1e3529db54 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -31,7 +31,8 @@ module ActiveRecord end def test_unknown_commands_delegate - recorder = CommandRecorder.new(stub(:foo => 'bar')) + recorder = Struct.new(:foo) + recorder = CommandRecorder.new(recorder.new('bar')) assert_equal 'bar', recorder.foo end @@ -169,6 +170,16 @@ module ActiveRecord end end + def test_invert_change_column_default_with_from_and_to + change = @recorder.inverse_of :change_column_default, [:table, :column, from: "old_value", to: "new_value"] + assert_equal [:change_column_default, [:table, :column, from: "new_value", to: "old_value"]], change + end + + def test_invert_change_column_default_with_from_and_to_with_boolean + change = @recorder.inverse_of :change_column_default, [:table, :column, from: true, to: false] + assert_equal [:change_column_default, [:table, :column, from: false, to: true]], change + end + def test_invert_change_column_null add = @recorder.inverse_of :change_column_null, [:table, :column, true] assert_equal [:change_column_null, [:table, :column, false]], add @@ -206,6 +217,11 @@ module ActiveRecord end def test_invert_remove_index + add = @recorder.inverse_of :remove_index, [:table, :one] + assert_equal [:add_index, [:table, :one]], add + end + + def test_invert_remove_index_with_column add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two], options: true}] assert_equal [:add_index, [:table, [:one, :two], options: true]], add end @@ -281,17 +297,42 @@ module ActiveRecord assert_equal [:remove_foreign_key, [:dogs, :people]], enable end + def test_invert_remove_foreign_key + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people] + assert_equal [:add_foreign_key, [:dogs, :people]], enable + end + def test_invert_add_foreign_key_with_column enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"] assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable end + def test_invert_remove_foreign_key_with_column + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, column: "owner_id"] + assert_equal [:add_foreign_key, [:dogs, :people, column: "owner_id"]], enable + end + def test_invert_add_foreign_key_with_column_and_name enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"] assert_equal [:remove_foreign_key, [:dogs, name: "fk"]], enable end - def test_remove_foreign_key_is_irreversible + def test_invert_remove_foreign_key_with_column_and_name + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"] + assert_equal [:add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"]], enable + end + + def test_invert_remove_foreign_key_with_primary_key + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, primary_key: "person_id"] + assert_equal [:add_foreign_key, [:dogs, :people, primary_key: "person_id"]], enable + end + + def test_invert_remove_foreign_key_with_on_delete_on_update + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade] + assert_equal [:add_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade]], enable + end + + def test_invert_remove_foreign_key_is_irreversible_without_to_table assert_raises ActiveRecord::IrreversibleMigration do @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"] end @@ -299,6 +340,10 @@ module ActiveRecord assert_raises ActiveRecord::IrreversibleMigration do @recorder.inverse_of :remove_foreign_key, [:dogs, name: "fk"] end + + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs] + end end end end diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb new file mode 100644 index 0000000000..60ca90464d --- /dev/null +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -0,0 +1,118 @@ +require 'cases/helper' + +module ActiveRecord + class Migration + class CompatibilityTest < ActiveRecord::TestCase + attr_reader :connection + self.use_transactional_tests = false + + def setup + super + @connection = ActiveRecord::Base.connection + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + connection.create_table :testings do |t| + t.column :foo, :string, :limit => 100 + t.column :bar, :string, :limit => 100 + end + end + + teardown do + connection.drop_table :testings rescue nil + ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::SchemaMigration.delete_all rescue nil + end + + def test_migration_doesnt_remove_named_index + connection.add_index :testings, :foo, :name => "custom_index_name" + + migration = Class.new(ActiveRecord::Migration[4.2]) { + def version; 101 end + def migrate(x) + remove_index :testings, :foo + end + }.new + + assert connection.index_exists?(:testings, :foo, name: "custom_index_name") + assert_raise(StandardError) { ActiveRecord::Migrator.new(:up, [migration]).migrate } + assert connection.index_exists?(:testings, :foo, name: "custom_index_name") + end + + def test_migration_does_remove_unnamed_index + connection.add_index :testings, :bar + + migration = Class.new(ActiveRecord::Migration[4.2]) { + def version; 101 end + def migrate(x) + remove_index :testings, :bar + end + }.new + + assert connection.index_exists?(:testings, :bar) + ActiveRecord::Migrator.new(:up, [migration]).migrate + assert_not connection.index_exists?(:testings, :bar) + end + + def test_references_does_not_add_index_by_default + migration = Class.new(ActiveRecord::Migration) { + def migrate(x) + create_table :more_testings do |t| + t.references :foo + t.belongs_to :bar, index: false + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert_not connection.index_exists?(:more_testings, :foo_id) + assert_not connection.index_exists?(:more_testings, :bar_id) + ensure + connection.drop_table :more_testings rescue nil + end + + def test_timestamps_have_null_constraints_if_not_present_in_migration_of_create_table + migration = Class.new(ActiveRecord::Migration) { + def migrate(x) + create_table :more_testings do |t| + t.timestamps + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.columns(:more_testings).find { |c| c.name == 'created_at' }.null + assert connection.columns(:more_testings).find { |c| c.name == 'updated_at' }.null + ensure + connection.drop_table :more_testings rescue nil + end + + def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table + migration = Class.new(ActiveRecord::Migration) { + def migrate(x) + add_timestamps :testings + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.columns(:testings).find { |c| c.name == 'created_at' }.null + assert connection.columns(:testings).find { |c| c.name == 'updated_at' }.null + end + + def test_legacy_migrations_get_deprecation_warning_when_run + migration = Class.new(ActiveRecord::Migration) { + def up + add_column :testings, :baz, :string + end + } + + assert_deprecated do + migration.migrate :up + end + end + end + end +end diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index 8fd08fe4ce..920c472c73 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -12,7 +12,9 @@ module ActiveRecord teardown do %w(artists_musics musics_videos catalog).each do |table_name| - connection.drop_table table_name if connection.tables.include?(table_name) + ActiveSupport::Deprecation.silence do + connection.drop_table table_name if connection.table_exists?(table_name) + end end end @@ -82,62 +84,69 @@ module ActiveRecord connection.create_join_table :artists, :musics connection.drop_join_table :artists, :musics - assert !connection.tables.include?('artists_musics') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') } end def test_drop_join_table_with_strings connection.create_join_table :artists, :musics connection.drop_join_table 'artists', 'musics' - assert !connection.tables.include?('artists_musics') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') } end def test_drop_join_table_with_the_proper_order connection.create_join_table :videos, :musics connection.drop_join_table :videos, :musics - assert !connection.tables.include?('musics_videos') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('musics_videos') } end def test_drop_join_table_with_the_table_name connection.create_join_table :artists, :musics, table_name: :catalog connection.drop_join_table :artists, :musics, table_name: :catalog - assert !connection.tables.include?('catalog') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('catalog') } end def test_drop_join_table_with_the_table_name_as_string connection.create_join_table :artists, :musics, table_name: 'catalog' connection.drop_join_table :artists, :musics, table_name: 'catalog' - assert !connection.tables.include?('catalog') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('catalog') } end def test_drop_join_table_with_column_options connection.create_join_table :artists, :musics, column_options: {null: true} connection.drop_join_table :artists, :musics, column_options: {null: true} - assert !connection.tables.include?('artists_musics') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') } end def test_create_and_drop_join_table_with_common_prefix with_table_cleanup do connection.create_join_table 'audio_artists', 'audio_musics' - assert_includes connection.tables, 'audio_artists_musics' + ActiveSupport::Deprecation.silence { assert connection.table_exists?('audio_artists_musics') } connection.drop_join_table 'audio_artists', 'audio_musics' - assert !connection.tables.include?('audio_artists_musics'), "Should have dropped join table, but didn't" + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('audio_artists_musics'), "Should have dropped join table, but didn't" } + end + end + + if current_adapter?(:PostgreSQLAdapter) + def test_create_join_table_with_uuid + connection.create_join_table :artists, :musics, column_options: { type: :uuid } + assert_equal [:uuid, :uuid], connection.columns(:artists_musics).map(&:type) end end private def with_table_cleanup - tables_before = connection.tables + tables_before = connection.data_sources yield ensure - tables_after = connection.tables - tables_before + tables_after = connection.data_sources - tables_before tables_after.each do |table| connection.drop_table table diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 7f4790bf3e..01162dcefe 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -99,7 +99,7 @@ module ActiveRecord assert_equal 1, foreign_keys.size fk = foreign_keys.first - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) # ON DELETE RESTRICT is the default on MySQL assert_equal nil, fk.on_delete else @@ -224,7 +224,7 @@ module ActiveRecord assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output end - class CreateCitiesAndHousesMigration < ActiveRecord::Migration + class CreateCitiesAndHousesMigration < ActiveRecord::Migration::Current def change create_table("cities") { |t| } @@ -243,6 +243,37 @@ module ActiveRecord silence_stream($stdout) { migration.migrate(:down) } end + class CreateSchoolsAndClassesMigration < ActiveRecord::Migration::Current + def change + create_table(:schools) + + create_table(:classes) do |t| + t.column :school_id, :integer + end + add_foreign_key :classes, :schools + end + end + + def test_add_foreign_key_with_prefix + ActiveRecord::Base.table_name_prefix = 'p_' + migration = CreateSchoolsAndClassesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("p_classes").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + ActiveRecord::Base.table_name_prefix = nil + end + + def test_add_foreign_key_with_suffix + ActiveRecord::Base.table_name_suffix = '_s' + migration = CreateSchoolsAndClassesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("classes_s").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + ActiveRecord::Base.table_name_suffix = nil + end + end end end diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb index 5bc0898f33..ad85684c0b 100644 --- a/activerecord/test/cases/migration/helper.rb +++ b/activerecord/test/cases/migration/helper.rb @@ -28,7 +28,7 @@ module ActiveRecord super TestModel.reset_table_name TestModel.reset_sequence_name - connection.drop_table :test_models rescue nil + connection.drop_table :test_models, if_exists: true end private diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index b23b9a679f..5abd37bfa2 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -130,7 +130,17 @@ module ActiveRecord def test_named_index_exists connection.add_index :testings, :foo, :name => "custom_index_name" + assert connection.index_exists?(:testings, :foo) assert connection.index_exists?(:testings, :foo, :name => "custom_index_name") + assert !connection.index_exists?(:testings, :foo, :name => "other_index_name") + end + + def test_remove_named_index + connection.add_index :testings, :foo, :name => "custom_index_name" + + assert connection.index_exists?(:testings, :foo) + connection.remove_index :testings, :foo + assert !connection.index_exists?(:testings, :foo) end def test_add_index_attribute_length_limit @@ -176,7 +186,7 @@ module ActiveRecord connection.remove_index("testings", :name => "named_admin") # Selected adapters support index sort order - if current_adapter?(:SQLite3Adapter, :MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + if current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter) connection.add_index("testings", ["last_name"], :order => {:last_name => :desc}) connection.remove_index("testings", ["last_name"]) connection.add_index("testings", ["last_name", "first_name"], :order => {:last_name => :desc}) diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb index 7afac83bd2..4f5589f32a 100644 --- a/activerecord/test/cases/migration/pending_migrations_test.rb +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -1,5 +1,4 @@ require 'cases/helper' -require "minitest/mock" module ActiveRecord class Migration diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 17ac72a109..9e19eb9f73 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -32,6 +32,14 @@ module ActiveRecord assert_equal [], @connection.foreign_keys("testings") end + test "foreign keys can be created in one query when index is not added" do + assert_queries(1) do + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: true, index: false + end + end + end + test "options hash can be passed" do @connection.change_table :testing_parents do |t| t.integer :other_id @@ -45,6 +53,15 @@ module ActiveRecord assert_equal "other_id", fk.primary_key end + test "to_table option can be passed" do + @connection.create_table :testings do |t| + t.references :parent, foreign_key: { to_table: :testing_parents } + end + fks = @connection.foreign_keys("testings") + assert_equal([["testings", "testing_parents", "parent_id"]], + fks.map {|fk| [fk.from_table, fk.to_table, fk.column] }) + end + test "foreign keys cannot be added to polymorphic relations when creating the table" do @connection.create_table :testings do |t| assert_raises(ArgumentError) do @@ -105,7 +122,95 @@ module ActiveRecord @connection.remove_reference :testings, :testing_parent, foreign_key: true end end + + test "foreign key methods respect pluralize_table_names" do + begin + original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names + ActiveRecord::Base.pluralize_table_names = false + @connection.create_table :testing + @connection.change_table :testing_parents do |t| + t.references :testing, foreign_key: true + end + + fk = @connection.foreign_keys("testing_parents").first + assert_equal "testing_parents", fk.from_table + assert_equal "testing", fk.to_table + + assert_difference "@connection.foreign_keys('testing_parents').size", -1 do + @connection.remove_reference :testing_parents, :testing, foreign_key: true + end + ensure + ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names + @connection.drop_table "testing", if_exists: true + end + end + + class CreateDogsMigration < ActiveRecord::Migration::Current + def change + create_table :dog_owners + + create_table :dogs do |t| + t.references :dog_owner, foreign_key: true + end + end + end + + def test_references_foreign_key_with_prefix + ActiveRecord::Base.table_name_prefix = 'p_' + migration = CreateDogsMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("p_dogs").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + ActiveRecord::Base.table_name_prefix = nil + end + + def test_references_foreign_key_with_suffix + ActiveRecord::Base.table_name_suffix = '_s' + migration = CreateDogsMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("dogs_s").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + ActiveRecord::Base.table_name_suffix = nil + end + + test "multiple foreign keys can be added to the same table" do + @connection.create_table :testings do |t| + t.integer :col_1 + t.integer :col_2 + + t.foreign_key :testing_parents, column: :col_1 + t.foreign_key :testing_parents, column: :col_2 + end + + fks = @connection.foreign_keys("testings") + + fk_definitions = fks.map {|fk| [fk.from_table, fk.to_table, fk.column] } + assert_equal([["testings", "testing_parents", "col_1"], + ["testings", "testing_parents", "col_2"]], fk_definitions) + end end end end +else +class ReferencesWithoutForeignKeySupportTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:testing_parents, force: true) + end + + teardown do + @connection.drop_table("testings", if_exists: true) + @connection.drop_table("testing_parents", if_exists: true) + end + + test "ignores foreign keys defined with the table" do + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: true + end + + assert_includes @connection.data_sources, "testings" + end +end end diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb index ad6b828d0b..a9a7f0f4c4 100644 --- a/activerecord/test/cases/migration/references_index_test.rb +++ b/activerecord/test/cases/migration/references_index_test.rb @@ -23,12 +23,12 @@ module ActiveRecord assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) end - def test_does_not_create_index + def test_creates_index_by_default_even_if_index_option_is_not_passed connection.create_table table_name do |t| t.references :foo end - assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) end def test_does_not_create_index_explicit @@ -68,13 +68,13 @@ module ActiveRecord assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) end - def test_does_not_create_index_for_existing_table + def test_creates_index_for_existing_table_even_if_index_option_is_not_passed connection.create_table table_name connection.change_table table_name do |t| t.references :foo end - assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) end def test_does_not_create_index_for_existing_table_explicit diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index f613fd66c3..70c64f3e71 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -30,14 +30,14 @@ module ActiveRecord assert column_exists?(table_name, :taggable_type, :string) end - def test_creates_reference_id_index - add_reference table_name, :user, index: true - assert index_exists?(table_name, :user_id) + def test_does_not_create_reference_id_index_if_index_is_false + add_reference table_name, :user, index: false + assert_not index_exists?(table_name, :user_id) end - def test_does_not_create_reference_id_index + def test_create_reference_id_index_even_if_index_option_is_passed add_reference table_name, :user - assert_not index_exists?(table_name, :user_id) + assert index_exists?(table_name, :user_id) end def test_creates_polymorphic_index @@ -55,6 +55,11 @@ module ActiveRecord assert index_exists?(table_name, :tag_id, name: 'index_taggings_on_tag_id') end + def test_creates_named_unique_index + add_reference table_name, :tag, index: { name: 'index_taggings_on_tag_id', unique: true } + assert index_exists?(table_name, :tag_id, name: 'index_taggings_on_tag_id', unique: true ) + end + def test_creates_reference_id_with_specified_type add_reference table_name, :user, type: :string assert column_exists?(table_name, :user_id, :string) diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 6d742d3f2f..b926a92849 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -15,7 +15,7 @@ module ActiveRecord end def teardown - rename_table :octopi, :test_models if connection.table_exists? :octopi + ActiveSupport::Deprecation.silence { rename_table :octopi, :test_models if connection.table_exists? :octopi } super end @@ -80,12 +80,10 @@ module ActiveRecord end def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences - enable_extension!('uuid-ossp', connection) connection.create_table :cats, id: :uuid assert_nothing_raised { rename_table :cats, :felines } - assert connection.table_exists? :felines + ActiveSupport::Deprecation.silence { assert connection.table_exists? :felines } ensure - disable_extension!('uuid-ossp', connection) connection.drop_table :cats, if_exists: true connection.drop_table :felines, if_exists: true end diff --git a/activerecord/test/cases/migration/table_and_index_test.rb b/activerecord/test/cases/migration/table_and_index_test.rb deleted file mode 100644 index 24cba84a09..0000000000 --- a/activerecord/test/cases/migration/table_and_index_test.rb +++ /dev/null @@ -1,24 +0,0 @@ -require "cases/helper" - -module ActiveRecord - class Migration - class TableAndIndexTest < ActiveRecord::TestCase - def test_add_schema_info_respects_prefix_and_suffix - conn = ActiveRecord::Base.connection - - conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name, if_exists: true) - # Use shorter prefix and suffix as in Oracle database identifier cannot be larger than 30 characters - ActiveRecord::Base.table_name_prefix = 'p_' - ActiveRecord::Base.table_name_suffix = '_s' - conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name, if_exists: true) - - conn.initialize_schema_migrations_table - - assert_equal "p_unique_schema_migrations_s", conn.indexes(ActiveRecord::Migrator.schema_migrations_table_name)[0][:name] - ensure - ActiveRecord::Base.table_name_prefix = "" - ActiveRecord::Base.table_name_suffix = "" - end - end - end -end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index b2f209fe97..a4b0de3f4e 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1,6 +1,7 @@ -require "cases/helper" -require "cases/migration/helper" +require 'cases/helper' +require 'cases/migration/helper' require 'bigdecimal/util' +require 'concurrent/atomic/count_down_latch' require 'models/person' require 'models/topic' @@ -68,6 +69,10 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migration.verbose = @verbose_was end + def test_migration_version_matches_component_version + assert_equal ActiveRecord::VERSION::STRING.to_f, ActiveRecord::Migration.current_version + end + def test_migrator_versions migrations_path = MIGRATIONS_ROOT + "/valid" old_path = ActiveRecord::Migrator.migrations_paths @@ -75,15 +80,13 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migrator.up(migrations_path) assert_equal 3, ActiveRecord::Migrator.current_version - assert_equal 3, ActiveRecord::Migrator.last_version assert_equal false, ActiveRecord::Migrator.needs_migration? ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid") assert_equal 0, ActiveRecord::Migrator.current_version - assert_equal 3, ActiveRecord::Migrator.last_version assert_equal true, ActiveRecord::Migrator.needs_migration? - ActiveRecord::SchemaMigration.create!(:version => ActiveRecord::Migrator.last_version) + ActiveRecord::SchemaMigration.create!(version: 3) assert_equal true, ActiveRecord::Migrator.needs_migration? ensure ActiveRecord::Migrator.migrations_paths = old_path @@ -115,7 +118,7 @@ class MigrationTest < ActiveRecord::TestCase end def test_migration_version - ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947) + assert_nothing_raised { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947) } end def test_create_table_with_force_true_does_not_drop_nonexisting_table @@ -132,17 +135,13 @@ class MigrationTest < ActiveRecord::TestCase Person.connection.drop_table :testings2, if_exists: true end - def connection - ActiveRecord::Base.connection - end - def test_migration_instance_has_connection - migration = Class.new(ActiveRecord::Migration).new - assert_equal connection, migration.connection + migration = Class.new(ActiveRecord::Migration::Current).new + assert_equal ActiveRecord::Base.connection, migration.connection end def test_method_missing_delegates_to_connection - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration::Current) { def connection Class.new { def create_table; "hi mom!"; end @@ -197,8 +196,6 @@ class MigrationTest < ActiveRecord::TestCase # of 0, they take on the compile-time limit for precision and scale, # so the following should succeed unless you have used really wacky # compilation options - # - SQLite2 has the default behavior of preserving all data sent in, - # so this happens there too assert_kind_of BigDecimal, b.value_of_e assert_equal BigDecimal("2.7182818284590452353602875"), b.value_of_e elsif current_adapter?(:SQLite3Adapter) @@ -231,7 +228,7 @@ class MigrationTest < ActiveRecord::TestCase assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } end - class MockMigration < ActiveRecord::Migration + class MockMigration < ActiveRecord::Migration::Current attr_reader :went_up, :went_down def initialize @went_up = false @@ -273,7 +270,7 @@ class MigrationTest < ActiveRecord::TestCase def test_migrator_one_up_with_exception_and_rollback assert_no_column Person, :last_name - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration::Current) { def version; 100 end def migrate(x) add_column "people", "last_name", :string @@ -294,7 +291,7 @@ class MigrationTest < ActiveRecord::TestCase def test_migrator_one_up_with_exception_and_rollback_using_run assert_no_column Person, :last_name - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration::Current) { def version; 100 end def migrate(x) add_column "people", "last_name", :string @@ -306,7 +303,7 @@ class MigrationTest < ActiveRecord::TestCase e = assert_raise(StandardError) { migrator.run } - assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message + assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message assert_no_column Person, :last_name, "On error, the Migrator should revert schema changes but it did not." @@ -315,7 +312,7 @@ class MigrationTest < ActiveRecord::TestCase def test_migration_without_transaction assert_no_column Person, :last_name - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration::Current) { self.disable_ddl_transaction! def version; 101 end @@ -359,6 +356,93 @@ class MigrationTest < ActiveRecord::TestCase Reminder.reset_table_name end + def test_internal_metadata_table_name + original_internal_metadata_table_name = ActiveRecord::Base.internal_metadata_table_name + + assert_equal "ar_internal_metadata", ActiveRecord::InternalMetadata.table_name + ActiveRecord::Base.table_name_prefix = "p_" + ActiveRecord::Base.table_name_suffix = "_s" + Reminder.reset_table_name + assert_equal "p_ar_internal_metadata_s", ActiveRecord::InternalMetadata.table_name + ActiveRecord::Base.internal_metadata_table_name = "changed" + Reminder.reset_table_name + assert_equal "p_changed_s", ActiveRecord::InternalMetadata.table_name + ActiveRecord::Base.table_name_prefix = "" + ActiveRecord::Base.table_name_suffix = "" + Reminder.reset_table_name + assert_equal "changed", ActiveRecord::InternalMetadata.table_name + ensure + ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name + Reminder.reset_table_name + end + + def test_internal_metadata_stores_environment + current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + migrations_path = MIGRATIONS_ROOT + "/valid" + old_path = ActiveRecord::Migrator.migrations_paths + ActiveRecord::Migrator.migrations_paths = migrations_path + + ActiveRecord::Migrator.up(migrations_path) + assert_equal current_env, ActiveRecord::InternalMetadata[:environment] + + original_rails_env = ENV["RAILS_ENV"] + original_rack_env = ENV["RACK_ENV"] + ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" + new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + + refute_equal current_env, new_env + + sleep 1 # mysql by default does not store fractional seconds in the database + ActiveRecord::Migrator.up(migrations_path) + assert_equal new_env, ActiveRecord::InternalMetadata[:environment] + ensure + ActiveRecord::Migrator.migrations_paths = old_path + ENV["RAILS_ENV"] = original_rails_env + ENV["RACK_ENV"] = original_rack_env + end + + + def test_migration_sets_internal_metadata_even_when_fully_migrated + current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + migrations_path = MIGRATIONS_ROOT + "/valid" + old_path = ActiveRecord::Migrator.migrations_paths + ActiveRecord::Migrator.migrations_paths = migrations_path + + ActiveRecord::Migrator.up(migrations_path) + assert_equal current_env, ActiveRecord::InternalMetadata[:environment] + + original_rails_env = ENV["RAILS_ENV"] + original_rack_env = ENV["RACK_ENV"] + ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" + new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + + refute_equal current_env, new_env + + sleep 1 # mysql by default does not store fractional seconds in the database + + ActiveRecord::Migrator.up(migrations_path) + assert_equal new_env, ActiveRecord::InternalMetadata[:environment] + ensure + ActiveRecord::Migrator.migrations_paths = old_path + ENV["RAILS_ENV"] = original_rails_env + ENV["RACK_ENV"] = original_rack_env + end + + def test_rename_internal_metadata_table + original_internal_metadata_table_name = ActiveRecord::Base.internal_metadata_table_name + + ActiveRecord::Base.internal_metadata_table_name = "active_record_internal_metadatas" + Reminder.reset_table_name + + ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name + Reminder.reset_table_name + + assert_equal "ar_internal_metadata", ActiveRecord::InternalMetadata.table_name + ensure + ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name + Reminder.reset_table_name + end + def test_proper_table_name_on_migration reminder_class = new_isolated_reminder_class migration = ActiveRecord::Migration.new @@ -436,13 +520,12 @@ class MigrationTest < ActiveRecord::TestCase data_column = columns.detect { |c| c.name == "data" } assert_nil data_column.default - + ensure Person.connection.drop_table :binary_testings, if_exists: true end unless mysql_enforcing_gtid_consistency? def test_create_table_with_query - Person.connection.drop_table :table_from_query_testings rescue nil Person.connection.create_table(:person, force: true) Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM person" @@ -450,12 +533,11 @@ class MigrationTest < ActiveRecord::TestCase columns = Person.connection.columns(:table_from_query_testings) assert_equal 1, columns.length assert_equal "id", columns.first.name - + ensure Person.connection.drop_table :table_from_query_testings rescue nil end def test_create_table_with_query_from_relation - Person.connection.drop_table :table_from_query_testings rescue nil Person.connection.create_table(:person, force: true) Person.connection.create_table :table_from_query_testings, as: Person.select(:id) @@ -463,7 +545,7 @@ class MigrationTest < ActiveRecord::TestCase columns = Person.connection.columns(:table_from_query_testings) assert_equal 1, columns.length assert_equal "id", columns.first.name - + ensure Person.connection.drop_table :table_from_query_testings rescue nil end end @@ -505,9 +587,8 @@ class MigrationTest < ActiveRecord::TestCase end end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) - def test_out_of_range_limit_should_raise - Person.connection.drop_table :test_limits rescue nil + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + def test_out_of_range_integer_limit_should_raise e = assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do Person.connection.create_table :test_integer_limits, :force => true do |t| t.column :bigone, :integer, :limit => 10 @@ -515,16 +596,94 @@ class MigrationTest < ActiveRecord::TestCase end assert_match(/No integer type has byte size 10/, e.message) + ensure + Person.connection.drop_table :test_integer_limits, if_exists: true + end + end - unless current_adapter?(:PostgreSQLAdapter) - assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do - Person.connection.create_table :test_text_limits, :force => true do |t| - t.column :bigtext, :text, :limit => 0xfffffffff - end + if current_adapter?(:Mysql2Adapter) + def test_out_of_range_text_limit_should_raise + e = assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do + Person.connection.create_table :test_text_limits, force: true do |t| + t.text :bigtext, limit: 0xfffffffff + end + end + + assert_match(/No text type has byte length #{0xfffffffff}/, e.message) + ensure + Person.connection.drop_table :test_text_limits, if_exists: true + end + end + + if ActiveRecord::Base.connection.supports_advisory_locks? + def test_migrator_generates_valid_lock_id + migration = Class.new(ActiveRecord::Migration::Current).new + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + + lock_id = migrator.send(:generate_migrator_advisory_lock_id) + + assert ActiveRecord::Base.connection.get_advisory_lock(lock_id), + "the Migrator should have generated a valid lock id, but it didn't" + assert ActiveRecord::Base.connection.release_advisory_lock(lock_id), + "the Migrator should have generated a valid lock id, but it didn't" + end + + def test_generate_migrator_advisory_lock_id + # It is important we are consistent with how we generate this so that + # exclusive locking works across migrator versions + migration = Class.new(ActiveRecord::Migration::Current).new + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + + lock_id = migrator.send(:generate_migrator_advisory_lock_id) + + current_database = ActiveRecord::Base.connection.current_database + salt = ActiveRecord::Migrator::MIGRATOR_SALT + expected_id = Zlib.crc32(current_database) * salt + + assert lock_id == expected_id, "expected lock id generated by the migrator to be #{expected_id}, but it was #{lock_id} instead" + assert lock_id.bit_length <= 63, "lock id must be a signed integer of max 63 bits magnitude" + end + + def test_migrator_one_up_with_unavailable_lock + assert_no_column Person, :last_name + + migration = Class.new(ActiveRecord::Migration::Current) { + def version; 100 end + def migrate(x) + add_column "people", "last_name", :string + end + }.new + + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + lock_id = migrator.send(:generate_migrator_advisory_lock_id) + + with_another_process_holding_lock(lock_id) do + assert_raise(ActiveRecord::ConcurrentMigrationError) { migrator.migrate } + end + + assert_no_column Person, :last_name, + "without an advisory lock, the Migrator should not make any changes, but it did." + end + + def test_migrator_one_up_with_unavailable_lock_using_run + assert_no_column Person, :last_name + + migration = Class.new(ActiveRecord::Migration::Current) { + def version; 100 end + def migrate(x) + add_column "people", "last_name", :string end + }.new + + migrator = ActiveRecord::Migrator.new(:up, [migration], 100) + lock_id = migrator.send(:generate_migrator_advisory_lock_id) + + with_another_process_holding_lock(lock_id) do + assert_raise(ActiveRecord::ConcurrentMigrationError) { migrator.run } end - Person.connection.drop_table :test_limits rescue nil + assert_no_column Person, :last_name, + "without an advisory lock, the Migrator should not make any changes, but it did." end end @@ -537,6 +696,30 @@ class MigrationTest < ActiveRecord::TestCase def self.base_class; self; end } end + + def with_another_process_holding_lock(lock_id) + thread_lock = Concurrent::CountDownLatch.new + test_terminated = Concurrent::CountDownLatch.new + + other_process = Thread.new do + begin + conn = ActiveRecord::Base.connection_pool.checkout + conn.get_advisory_lock(lock_id) + thread_lock.count_down + test_terminated.wait # hold the lock open until we tested everything + ensure + conn.release_advisory_lock(lock_id) + ActiveRecord::Base.connection_pool.checkin(conn) + end + end + + thread_lock.wait # wait until the 'other process' has the lock + + yield + + test_terminated.count_down + other_process.join + end end class ReservedWordsMigrationTest < ActiveRecord::TestCase @@ -550,7 +733,7 @@ class ReservedWordsMigrationTest < ActiveRecord::TestCase connection.add_index :values, :value connection.remove_index :values, :column => :value end - + ensure connection.drop_table :values rescue nil end end @@ -562,11 +745,11 @@ class ExplicitlyNamedIndexMigrationTest < ActiveRecord::TestCase t.integer :value end - assert_nothing_raised ArgumentError do + assert_nothing_raised do connection.add_index :values, :value, name: 'a_different_name' connection.remove_index :values, column: :value, name: 'a_different_name' end - + ensure connection.drop_table :values rescue nil end end @@ -928,4 +1111,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase ActiveRecord::Base.logger = old end + def test_unknown_migration_version_should_raise_an_argument_error + assert_raise(ArgumentError) { ActiveRecord::Migration[1.0] } + end end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 2ff6938e7b..86eca53141 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -6,7 +6,7 @@ class MigratorTest < ActiveRecord::TestCase # Use this class to sense if migrations have gone # up or down. - class Sensor < ActiveRecord::Migration + class Sensor < ActiveRecord::Migration::Current attr_reader :went_up, :went_down def initialize name = self.class.name, version = nil @@ -313,9 +313,9 @@ class MigratorTest < ActiveRecord::TestCase _, migrator = migrator_class(3) ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true - assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations') + ActiveSupport::Deprecation.silence { assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations') } migrator.migrate("valid", 1) - assert ActiveRecord::Base.connection.table_exists?('schema_migrations') + ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?('schema_migrations') } end def test_migrator_forward diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index 6f65bf80eb..486bcc22df 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -63,17 +63,16 @@ class ModulesTest < ActiveRecord::TestCase def test_assign_ids firm = MyApplication::Business::Firm.first - assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do + assert_nothing_raised do firm.client_ids = [MyApplication::Business::Client.first.id] end end - # need to add an eager loading condition to force the eager loading model into - # the old join model, to test that. See http://dev.rubyonrails.org/ticket/9640 + # An eager loading condition to force the eager loading model into the old join model. def test_eager_loading_in_modules clients = [] - assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do + assert_nothing_raised do clients << MyApplication::Business::Client.references(:accounts).merge!(:includes => {:firm => :account}, :where => 'accounts.id IS NOT NULL').find(3) clients << MyApplication::Business::Client.includes(:firm => :account).find(3) end diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index 39cdcf5403..a4fbf579a1 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -24,6 +24,13 @@ class MultipleDbTest < ActiveRecord::TestCase assert_equal(ActiveRecord::Base.connection, Entrant.connection) end + def test_swapping_the_connection + old_spec_name, Course.connection_specification_name = Course.connection_specification_name, "primary" + assert_equal(Entrant.connection, Course.connection) + ensure + Course.connection_specification_name = old_spec_name + end + def test_find c1 = Course.find(1) assert_equal "Ruby Development", c1.name @@ -89,8 +96,8 @@ class MultipleDbTest < ActiveRecord::TestCase end def test_connection - assert_equal Entrant.arel_engine.connection, Bird.arel_engine.connection - assert_not_equal Entrant.arel_engine.connection, Course.arel_engine.connection + assert_equal Entrant.arel_engine.connection.object_id, Bird.arel_engine.connection.object_id + assert_not_equal Entrant.arel_engine.connection.object_id, Course.arel_engine.connection.object_id end unless in_memory_db? @@ -104,7 +111,7 @@ class MultipleDbTest < ActiveRecord::TestCase def test_associations_should_work_when_model_has_no_connection begin ActiveRecord::Base.remove_connection - assert_nothing_raised ActiveRecord::ConnectionNotEstablished do + assert_nothing_raised do College.first.courses.first end ensure diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 6b4addd52f..11fb164d50 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -61,6 +61,13 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase assert_equal "No association found for name `honesty'. Has it been defined yet?", exception.message end + def test_should_raise_an_UnknownAttributeError_for_non_existing_nested_attributes + exception = assert_raise ActiveModel::UnknownAttributeError do + Pirate.new(:ship_attributes => { :sail => true }) + end + assert_equal "unknown attribute 'sail' for Ship.", exception.message + end + def test_should_disable_allow_destroy_by_default Pirate.accepts_nested_attributes_for :ship @@ -69,7 +76,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase pirate.update(ship_attributes: { '_destroy' => true, :id => ship.id }) - assert_nothing_raised(ActiveRecord::RecordNotFound) { pirate.ship.reload } + assert_nothing_raised { pirate.ship.reload } end def test_a_model_should_respond_to_underscore_destroy_and_return_if_it_is_marked_for_destruction @@ -146,6 +153,19 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase assert man.reload.interests.empty? end + def test_reject_if_is_not_short_circuited_if_allow_destroy_is_false + Pirate.accepts_nested_attributes_for :ship, reject_if: ->(a) { a[:name] == "The Golden Hind" }, allow_destroy: false + + pirate = Pirate.create!(catchphrase: "Stop wastin' me time", ship_attributes: { name: "White Pearl", _destroy: "1" }) + assert_equal "White Pearl", pirate.reload.ship.name + + pirate.update!(ship_attributes: { id: pirate.ship.id, name: "The Golden Hind", _destroy: "1" }) + assert_equal "White Pearl", pirate.reload.ship.name + + pirate.update!(ship_attributes: { id: pirate.ship.id, name: "Black Pearl", _destroy: "1" }) + assert_equal "Black Pearl", pirate.reload.ship.name + end + def test_has_many_association_updating_a_single_record Man.accepts_nested_attributes_for(:interests) man = Man.create(name: 'John') @@ -160,7 +180,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase pirate = Pirate.new(:catchphrase => "Stop wastin' me time") pirate.ship_attributes = { :id => "" } - assert_nothing_raised(ActiveRecord::RecordNotFound) { pirate.save! } + assert_nothing_raised { pirate.save! } end def test_first_and_array_index_zero_methods_return_the_same_value_when_nested_attributes_are_set_to_update_existing_record @@ -273,10 +293,11 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase end def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id - @ship.stubs(:id).returns('ABC1X') - @pirate.ship_attributes = { :id => @ship.id, :name => 'Davy Jones Gold Dagger' } + @ship.stub(:id, 'ABC1X') do + @pirate.ship_attributes = { :id => @ship.id, :name => 'Davy Jones Gold Dagger' } - assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name + assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name + end end def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy_is_truthy @@ -457,10 +478,11 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase end def test_should_modify_an_existing_record_if_there_is_a_matching_composite_id - @pirate.stubs(:id).returns('ABC1X') - @ship.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' } + @pirate.stub(:id, 'ABC1X') do + @ship.pirate_attributes = { :id => @pirate.id, :catchphrase => 'Arr' } - assert_equal 'Arr', @ship.pirate.catchphrase + assert_equal 'Arr', @ship.pirate.catchphrase + end end def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy_is_truthy @@ -489,7 +511,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy [nil, '0', 0, 'false', false].each do |not_truth| @ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: not_truth }) - assert_nothing_raised(ActiveRecord::RecordNotFound) { @ship.pirate.reload } + assert_nothing_raised { @ship.pirate.reload } end end @@ -497,7 +519,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase Ship.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc(&:empty?) @ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: '1' }) - assert_nothing_raised(ActiveRecord::RecordNotFound) { @ship.pirate.reload } + assert_nothing_raised { @ship.pirate.reload } ensure Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc(&:empty?) end @@ -514,7 +536,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase pirate = @ship.pirate @ship.attributes = { :pirate_attributes => { :id => pirate.id, '_destroy' => true } } - assert_nothing_raised(ActiveRecord::RecordNotFound) { Pirate.find(pirate.id) } + assert_nothing_raised { Pirate.find(pirate.id) } @ship.save assert_raise(ActiveRecord::RecordNotFound) { Pirate.find(pirate.id) } end @@ -567,6 +589,13 @@ module NestedAttributesOnACollectionAssociationTests assert_respond_to @pirate, association_setter end + def test_should_raise_an_UnknownAttributeError_for_non_existing_nested_attributes_for_has_many + exception = assert_raise ActiveModel::UnknownAttributeError do + @pirate.parrots_attributes = [{ peg_leg: true }] + end + assert_equal "unknown attribute 'peg_leg' for Parrot.", exception.message + end + def test_should_save_only_one_association_on_create pirate = Pirate.create!({ :catchphrase => 'Arr', @@ -638,17 +667,19 @@ module NestedAttributesOnACollectionAssociationTests end def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models - @child_1.stubs(:id).returns('ABC1X') - @child_2.stubs(:id).returns('ABC2X') - - @pirate.attributes = { - association_getter => [ - { :id => @child_1.id, :name => 'Grace OMalley' }, - { :id => @child_2.id, :name => 'Privateers Greed' } - ] - } + @child_1.stub(:id, 'ABC1X') do + @child_2.stub(:id, 'ABC2X') do + + @pirate.attributes = { + association_getter => [ + { :id => @child_1.id, :name => 'Grace OMalley' }, + { :id => @child_2.id, :name => 'Privateers Greed' } + ] + } - assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name] + assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.name, @child_2.name] + end + end end def test_should_raise_RecordNotFound_if_an_id_is_given_but_doesnt_return_a_record @@ -658,6 +689,16 @@ module NestedAttributesOnACollectionAssociationTests assert_equal "Couldn't find #{@child_1.class.name} with ID=1234567890 for Pirate with ID=#{@pirate.id}", exception.message end + def test_should_raise_RecordNotFound_if_an_id_belonging_to_a_different_record_is_given + other_pirate = Pirate.create! catchphrase: 'Ahoy!' + other_child = other_pirate.send(@association_name).create! name: 'Buccaneers Servant' + + exception = assert_raise ActiveRecord::RecordNotFound do + @pirate.attributes = { association_getter => [{ id: other_child.id }] } + end + assert_equal "Couldn't find #{@child_1.class.name} with ID=#{other_child.id} for Pirate with ID=#{@pirate.id}", exception.message + end + def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_is_missing @pirate.send(@association_name).destroy_all @pirate.reload.attributes = { @@ -672,7 +713,7 @@ module NestedAttributesOnACollectionAssociationTests end def test_should_not_assign_destroy_key_to_a_record - assert_nothing_raised ActiveRecord::UnknownAttributeError do + assert_nothing_raised do @pirate.send(association_setter, { 'foo' => { '_destroy' => '0' }}) end end @@ -707,8 +748,8 @@ module NestedAttributesOnACollectionAssociationTests end def test_should_raise_an_argument_error_if_something_else_than_a_hash_is_passed - assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, {}) } - assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, Hash.new) } + assert_nothing_raised { @pirate.send(association_setter, {}) } + assert_nothing_raised { @pirate.send(association_setter, Hash.new) } exception = assert_raise ArgumentError do @pirate.send(association_setter, "foo") @@ -783,7 +824,7 @@ module NestedAttributesOnACollectionAssociationTests def test_can_use_symbols_as_object_identifier @pirate.attributes = { :parrots_attributes => { :foo => { :name => 'Lovely Day' }, :bar => { :name => 'Blown Away' } } } - assert_nothing_raised(NoMethodError) { @pirate.save! } + assert_nothing_raised { @pirate.save! } end def test_numeric_column_changes_from_zero_to_no_empty_string diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 2370077eb0..56092aaa0c 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -17,7 +17,10 @@ require 'models/minivan' require 'models/owner' require 'models/person' require 'models/pet' +require 'models/ship' require 'models/toy' +require 'models/admin' +require 'models/admin/user' require 'rexml/document' class PersistenceTest < ActiveRecord::TestCase @@ -119,13 +122,22 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal 59, accounts(:signals37, :reload).credit_limit end + def test_increment_updates_counter_in_db_using_offset + a1 = accounts(:signals37) + initial_credit = a1.credit_limit + a2 = Account.find(accounts(:signals37).id) + a1.increment!(:credit_limit) + a2.increment!(:credit_limit) + assert_equal initial_credit + 2, a1.reload.credit_limit + end + def test_destroy_all conditions = "author_name = 'Mary'" topics_by_mary = Topic.all.merge!(:where => conditions, :order => 'id').to_a assert ! topics_by_mary.empty? assert_difference('Topic.count', -topics_by_mary.size) do - destroyed = Topic.destroy_all(conditions).sort_by(&:id) + destroyed = Topic.where(conditions).destroy_all.sort_by(&:id) assert_equal topics_by_mary, destroyed assert destroyed.all?(&:frozen?), "destroyed topics should be frozen" end @@ -151,10 +163,27 @@ class PersistenceTest < ActiveRecord::TestCase assert !company.valid? original_errors = company.errors client = company.becomes(Client) - assert_equal original_errors, client.errors + assert_equal original_errors.keys, client.errors.keys + end + + def test_becomes_errors_base + child_class = Class.new(Admin::User) do + store_accessor :settings, :foo + + def self.name; 'Admin::ChildUser'; end + end + + admin = Admin::User.new + admin.errors.add :token, :invalid + child = admin.becomes(child_class) + + assert_equal [:token], child.errors.keys + assert_nothing_raised do + child.errors.add :foo, :invalid + end end - def test_dupd_becomes_persists_changes_from_the_original + def test_duped_becomes_persists_changes_from_the_original original = topics(:first) copy = original.dup.becomes(Reply) copy.save! @@ -734,9 +763,10 @@ class PersistenceTest < ActiveRecord::TestCase assert !topic.approved? assert_equal "The First Topic", topic.title - assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do + error = assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do topic.update_attributes(id: 3, title: "Hm is it possible?") end + assert_not_nil error.cause assert_not_equal "Hm is it possible?", Topic.find(3).title topic.update_attributes(id: 1234) @@ -897,6 +927,33 @@ class PersistenceTest < ActiveRecord::TestCase assert_not post.new_record? end + def test_reload_via_querycache + ActiveRecord::Base.connection.enable_query_cache! + ActiveRecord::Base.connection.clear_query_cache + assert ActiveRecord::Base.connection.query_cache_enabled, 'cache should be on' + parrot = Parrot.create(:name => 'Shane') + + # populate the cache with the SELECT result + found_parrot = Parrot.find(parrot.id) + assert_equal parrot.id, found_parrot.id + + # Manually update the 'name' attribute in the DB directly + assert_equal 1, ActiveRecord::Base.connection.query_cache.length + ActiveRecord::Base.uncached do + found_parrot.name = 'Mary' + found_parrot.save + end + + # Now reload, and verify that it gets the DB version, and not the querycache version + found_parrot.reload + assert_equal 'Mary', found_parrot.name + + found_parrot = Parrot.find(parrot.id) + assert_equal 'Mary', found_parrot.name + ensure + ActiveRecord::Base.connection.disable_query_cache! + end + class SaveTest < ActiveRecord::TestCase self.use_transactional_tests = false @@ -924,7 +981,21 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal instance.created_at, created_at assert_equal instance.updated_at, updated_at ensure - ActiveRecord::Base.connection.drop_table :widgets + ActiveRecord::Base.connection.drop_table widget.table_name + widget.reset_column_information end end + + def test_reset_column_information_resets_children + child = Class.new(Topic) + child.new # force schema to load + + ActiveRecord::Base.connection.add_column(:topics, :foo, :string) + Topic.reset_column_information + + assert_equal "bar", child.new(foo: :bar).foo + ensure + ActiveRecord::Base.connection.remove_column(:topics, :foo) + Topic.reset_column_information + end end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index daa3271777..bca50dd008 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -44,7 +44,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase conn = ActiveRecord::Base.connection_pool.checkout ActiveRecord::Base.connection_pool.checkin conn @connection_count += 1 - ActiveRecord::Base.connection.tables + ActiveRecord::Base.connection.data_sources rescue ActiveRecord::ConnectionTimeoutError @timed_out += 1 end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 3664a2af70..52eac4a124 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -130,27 +130,25 @@ class PrimaryKeysTest < ActiveRecord::TestCase end def test_supports_primary_key - assert_nothing_raised NoMethodError do + assert_nothing_raised do ActiveRecord::Base.connection.supports_primary_key? end end - def test_primary_key_returns_value_if_it_exists - klass = Class.new(ActiveRecord::Base) do - self.table_name = 'developers' - end + if ActiveRecord::Base.connection.supports_primary_key? + def test_primary_key_returns_value_if_it_exists + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers' + end - if ActiveRecord::Base.connection.supports_primary_key? assert_equal 'id', klass.primary_key end - end - def test_primary_key_returns_nil_if_it_does_not_exist - klass = Class.new(ActiveRecord::Base) do - self.table_name = 'developers_projects' - end + def test_primary_key_returns_nil_if_it_does_not_exist + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers_projects' + end - if ActiveRecord::Base.connection.supports_primary_key? assert_nil klass.primary_key end end @@ -175,6 +173,20 @@ class PrimaryKeysTest < ActiveRecord::TestCase dashboard = Dashboard.first assert_equal '2', dashboard.id end + + if current_adapter?(:PostgreSQLAdapter) + def test_serial_with_quoted_sequence_name + column = MixedCaseMonkey.columns_hash[MixedCaseMonkey.primary_key] + assert_equal "nextval('\"mixed_case_monkeys_monkeyID_seq\"'::regclass)", column.default_function + assert column.serial? + end + + def test_serial_with_unquoted_sequence_name + column = Topic.columns_hash[Topic.primary_key] + assert_equal "nextval('topics_id_seq'::regclass)", column.default_function + assert column.serial? + end + end end class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase @@ -210,15 +222,16 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase end teardown do - @connection.drop_table(:barcodes) if @connection.table_exists? :barcodes + @connection.drop_table(:barcodes, if_exists: true) end def test_any_type_primary_key assert_equal "code", Barcode.primary_key - column_type = Barcode.type_for_attribute(Barcode.primary_key) - assert_equal :string, column_type.type - assert_equal 42, column_type.limit + column = Barcode.column_for_attribute(Barcode.primary_key) + assert_not column.null + assert_equal :string, column.type + assert_equal 42, column.limit end test "schema dump primary key includes type and options" do @@ -227,7 +240,41 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase end end -if current_adapter?(:MysqlAdapter, :Mysql2Adapter) +class CompositePrimaryKeyTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + self.use_transactional_tests = false + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table(:barcodes, primary_key: ["region", "code"], force: true) do |t| + t.string :region + t.integer :code + end + end + + def teardown + @connection.drop_table(:barcodes, if_exists: true) + end + + def test_composite_primary_key + assert_equal ["region", "code"], @connection.primary_keys("barcodes") + end + + def test_primary_key_issues_warning + warning = capture(:stderr) do + assert_nil @connection.primary_key("barcodes") + end + assert_match(/WARNING: Rails does not support composite primary key\./, warning) + end + + def test_collectly_dump_composite_primary_key + schema = dump_table_schema "barcodes" + assert_match %r{create_table "barcodes", primary_key: \["region", "code"\]}, schema + end +end + +if current_adapter?(:Mysql2Adapter) class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase self.use_transactional_tests = false @@ -239,9 +286,35 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) con.reconnect! end end + + class PrimaryKeyBigintNilDefaultTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + self.use_transactional_tests = false + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table(:bigint_defaults, id: :bigint, default: nil, force: true) + end + + def teardown + @connection.drop_table :bigint_defaults, if_exists: true + end + + test "primary key with bigint allows default override via nil" do + column = @connection.columns(:bigint_defaults).find { |c| c.name == 'id' } + assert column.bigint? + assert_not column.auto_increment? + end + + test "schema dump primary key with bigint default nil" do + schema = dump_table_schema "bigint_defaults" + assert_match %r{create_table "bigint_defaults", id: :bigint, default: nil}, schema + end + end end -if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) +if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) class PrimaryKeyBigSerialTest < ActiveRecord::TestCase include SchemaDumpingHelper @@ -260,7 +333,8 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) end teardown do - @connection.drop_table 'widgets', if_exists: true + @connection.drop_table :widgets, if_exists: true + Widget.reset_column_information end test "primary key column type with bigserial" do @@ -277,19 +351,20 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) test "schema dump primary key with bigserial" do schema = dump_table_schema "widgets" if current_adapter?(:PostgreSQLAdapter) - assert_match %r{create_table "widgets", id: :bigserial}, schema + assert_match %r{create_table "widgets", id: :bigserial, force: :cascade}, schema else - assert_match %r{create_table "widgets", id: :bigint}, schema + assert_match %r{create_table "widgets", id: :bigint, force: :cascade}, schema end end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) test "primary key column type with options" do - @connection.create_table(:widgets, id: :primary_key, limit: 8, force: true) + @connection.create_table(:widgets, id: :primary_key, limit: 8, unsigned: true, force: true) column = @connection.columns(:widgets).find { |c| c.name == 'id' } assert column.auto_increment? assert_equal :integer, column.type assert_equal 8, column.limit + assert column.unsigned? end end end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 744f9edc47..e53239cdee 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -16,7 +16,7 @@ class QueryCacheTest < ActiveRecord::TestCase def test_exceptional_middleware_clears_and_disables_cache_on_error assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off' - mw = ActiveRecord::QueryCache.new lambda { |env| + mw = middleware { |env| Task.find 1 Task.find 1 assert_equal 1, ActiveRecord::Base.connection.query_cache.length @@ -31,7 +31,7 @@ class QueryCacheTest < ActiveRecord::TestCase def test_exceptional_middleware_leaves_enabled_cache_alone ActiveRecord::Base.connection.enable_query_cache! - mw = ActiveRecord::QueryCache.new lambda { |env| + mw = middleware { |env| raise "lol borked" } assert_raises(RuntimeError) { mw.call({}) } @@ -42,7 +42,7 @@ class QueryCacheTest < ActiveRecord::TestCase def test_exceptional_middleware_assigns_original_connection_id_on_error connection_id = ActiveRecord::Base.connection_id - mw = ActiveRecord::QueryCache.new lambda { |env| + mw = middleware { |env| ActiveRecord::Base.connection_id = self.object_id raise "lol borked" } @@ -53,7 +53,7 @@ class QueryCacheTest < ActiveRecord::TestCase def test_middleware_delegates called = false - mw = ActiveRecord::QueryCache.new lambda { |env| + mw = middleware { |env| called = true [200, {}, nil] } @@ -62,7 +62,7 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_middleware_caches - mw = ActiveRecord::QueryCache.new lambda { |env| + mw = middleware { |env| Task.find 1 Task.find 1 assert_equal 1, ActiveRecord::Base.connection.query_cache.length @@ -74,50 +74,13 @@ class QueryCacheTest < ActiveRecord::TestCase def test_cache_enabled_during_call assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache off' - mw = ActiveRecord::QueryCache.new lambda { |env| + mw = middleware { |env| assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on' [200, {}, nil] } mw.call({}) end - def test_cache_on_during_body_write - streaming = Class.new do - def each - yield ActiveRecord::Base.connection.query_cache_enabled - end - end - - mw = ActiveRecord::QueryCache.new lambda { |env| - [200, {}, streaming.new] - } - body = mw.call({}).last - body.each { |x| assert x, 'cache should be on' } - body.close - assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache disabled' - end - - def test_cache_off_after_close - mw = ActiveRecord::QueryCache.new lambda { |env| [200, {}, nil] } - body = mw.call({}).last - - assert ActiveRecord::Base.connection.query_cache_enabled, 'cache enabled' - body.close - assert !ActiveRecord::Base.connection.query_cache_enabled, 'cache disabled' - end - - def test_cache_clear_after_close - mw = ActiveRecord::QueryCache.new lambda { |env| - Post.first - [200, {}, nil] - } - body = mw.call({}).last - - assert !ActiveRecord::Base.connection.query_cache.empty?, 'cache not empty' - body.close - assert ActiveRecord::Base.connection.query_cache.empty?, 'cache should be empty' - end - def test_cache_passing_a_relation post = Post.first Post.cache do @@ -184,7 +147,7 @@ class QueryCacheTest < ActiveRecord::TestCase # Oracle adapter returns count() as Fixnum or Float if current_adapter?(:OracleAdapter) assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") - elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter) + elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter) # Future versions of the sqlite3 adapter will return numeric assert_instance_of Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") @@ -244,6 +207,13 @@ class QueryCacheTest < ActiveRecord::TestCase assert_equal 0, Post.where(title: 'rollback').to_a.count end end + + private + def middleware(&app) + executor = Class.new(ActiveSupport::Executor) + ActiveRecord::QueryCache.install_executor_hooks executor + lambda { |env| executor.wrap { app.call(env) } } + end end class QueryCacheExpiryTest < ActiveRecord::TestCase @@ -262,61 +232,66 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase end def test_find - Task.connection.expects(:clear_query_cache).times(1) + assert_called(Task.connection, :clear_query_cache) do + assert !Task.connection.query_cache_enabled + Task.cache do + assert Task.connection.query_cache_enabled + Task.find(1) - assert !Task.connection.query_cache_enabled - Task.cache do - assert Task.connection.query_cache_enabled - Task.find(1) + Task.uncached do + assert !Task.connection.query_cache_enabled + Task.find(1) + end - Task.uncached do - assert !Task.connection.query_cache_enabled - Task.find(1) + assert Task.connection.query_cache_enabled end - - assert Task.connection.query_cache_enabled + assert !Task.connection.query_cache_enabled end - assert !Task.connection.query_cache_enabled end def test_update - Task.connection.expects(:clear_query_cache).times(2) - Task.cache do - task = Task.find(1) - task.starting = Time.now.utc - task.save! + assert_called(Task.connection, :clear_query_cache, times: 2) do + Task.cache do + task = Task.find(1) + task.starting = Time.now.utc + task.save! + end end end def test_destroy - Task.connection.expects(:clear_query_cache).times(2) - Task.cache do - Task.find(1).destroy + assert_called(Task.connection, :clear_query_cache, times: 2) do + Task.cache do + Task.find(1).destroy + end end end def test_insert - ActiveRecord::Base.connection.expects(:clear_query_cache).times(2) - Task.cache do - Task.create! + assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do + Task.cache do + Task.create! + end end end def test_cache_is_expired_by_habtm_update - ActiveRecord::Base.connection.expects(:clear_query_cache).times(2) - ActiveRecord::Base.cache do - c = Category.first - p = Post.first - p.categories << c + assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do + ActiveRecord::Base.cache do + c = Category.first + p = Post.first + p.categories << c + end end end def test_cache_is_expired_by_habtm_delete - ActiveRecord::Base.connection.expects(:clear_query_cache).times(2) - ActiveRecord::Base.cache do - p = Post.find(1) - assert p.categories.any? - p.categories.delete_all + assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do + ActiveRecord::Base.cache do + p = Post.find(1) + assert p.categories.any? + p.categories.delete_all + end end end end diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index 1c919f0b57..5f6eb41240 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -7,6 +7,7 @@ require 'models/computer' require 'models/project' require 'models/reader' require 'models/person' +require 'models/ship' class ReadOnlyTest < ActiveRecord::TestCase fixtures :authors, :posts, :comments, :developers, :projects, :developers_projects, :people, :readers diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 7b47c80331..710c86b151 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -23,6 +23,7 @@ require 'models/chef' require 'models/department' require 'models/cake_designer' require 'models/drink_designer' +require 'models/mocktail_designer' require 'models/recipe' class ReflectionTest < ActiveRecord::TestCase @@ -278,6 +279,15 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal 2, @hotel.chefs.size end + def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case_and_sti + @hotel = Hotel.create! + @hotel.mocktail_designers << MocktailDesigner.create! + + assert_equal 1, @hotel.mocktail_designers.size + assert_equal 1, @hotel.mocktail_designers.count + assert_equal 1, @hotel.chef_lists.size + end + def test_scope_chain_of_polymorphic_association_does_not_leak_into_other_hmt_associations hotel = Hotel.create! department = hotel.departments.create! @@ -393,12 +403,14 @@ class ReflectionTest < ActiveRecord::TestCase product = Struct.new(:table_name, :pluralize_table_names).new('products', true) reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product) - reflection.stubs(:klass).returns(category) - assert_equal 'categories_products', reflection.join_table + reflection.stub(:klass, category) do + assert_equal 'categories_products', reflection.join_table + end reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category) - reflection.stubs(:klass).returns(product) - assert_equal 'categories_products', reflection.join_table + reflection.stub(:klass, product) do + assert_equal 'categories_products', reflection.join_table + end end def test_join_table_with_common_prefix @@ -406,12 +418,14 @@ class ReflectionTest < ActiveRecord::TestCase product = Struct.new(:table_name, :pluralize_table_names).new('catalog_products', true) reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product) - reflection.stubs(:klass).returns(category) - assert_equal 'catalog_categories_products', reflection.join_table + reflection.stub(:klass, category) do + assert_equal 'catalog_categories_products', reflection.join_table + end reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category) - reflection.stubs(:klass).returns(product) - assert_equal 'catalog_categories_products', reflection.join_table + reflection.stub(:klass, product) do + assert_equal 'catalog_categories_products', reflection.join_table + end end def test_join_table_with_different_prefix @@ -419,12 +433,14 @@ class ReflectionTest < ActiveRecord::TestCase page = Struct.new(:table_name, :pluralize_table_names).new('content_pages', true) reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, page) - reflection.stubs(:klass).returns(category) - assert_equal 'catalog_categories_content_pages', reflection.join_table + reflection.stub(:klass, category) do + assert_equal 'catalog_categories_content_pages', reflection.join_table + end reflection = ActiveRecord::Reflection.create(:has_many, :pages, nil, {}, category) - reflection.stubs(:klass).returns(page) - assert_equal 'catalog_categories_content_pages', reflection.join_table + reflection.stub(:klass, page) do + assert_equal 'catalog_categories_content_pages', reflection.join_table + end end def test_join_table_can_be_overridden @@ -432,12 +448,14 @@ class ReflectionTest < ActiveRecord::TestCase product = Struct.new(:table_name, :pluralize_table_names).new('products', true) reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, { :join_table => 'product_categories' }, product) - reflection.stubs(:klass).returns(category) - assert_equal 'product_categories', reflection.join_table + reflection.stub(:klass, category) do + assert_equal 'product_categories', reflection.join_table + end reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, { :join_table => 'product_categories' }, category) - reflection.stubs(:klass).returns(product) - assert_equal 'product_categories', reflection.join_table + reflection.stub(:klass, product) do + assert_equal 'product_categories', reflection.join_table + end end def test_includes_accepts_symbols diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 29c9d0e2af..f0e07e0731 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -27,8 +27,8 @@ module ActiveRecord module DelegationWhitelistBlacklistTests ARRAY_DELEGATES = [ - :+, :-, :|, :&, :[], - :all?, :collect, :detect, :each, :each_cons, :each_with_index, + :+, :-, :|, :&, :[], :shuffle, + :all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index, :exclude?, :find_all, :flat_map, :group_by, :include?, :length, :map, :none?, :one?, :partition, :reject, :reverse, :sample, :second, :sort, :sort_by, :third, @@ -40,12 +40,6 @@ module ActiveRecord assert_respond_to target, method end end - - ActiveRecord::Delegation::BLACKLISTED_ARRAY_METHODS.each do |method| - define_method "test_#{method}_is_not_delegated_to_Array" do - assert_raises(NoMethodError) { call_method(target, method) } - end - end end class DelegationAssociationTest < DelegationTest diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index 0a2e874e4f..60a806c05a 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -104,6 +104,13 @@ class RelationMergingTest < ActiveRecord::TestCase post = PostThatLoadsCommentsInAnAfterSaveHook.create!(title: "First Post", body: "Blah blah blah.") assert_equal "First comment!", post.comments.where(body: "First comment!").first_or_create.body end + + def test_merging_with_from_clause + relation = Post.all + assert relation.from_clause.empty? + relation = relation.merge(Post.from("posts")) + refute relation.from_clause.empty? + end end class MergingDifferentRelationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 45ead08bd5..ffb2da7a26 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -22,13 +22,21 @@ module ActiveRecord def sanitize_sql(sql) sql end + + def sanitize_sql_for_order(sql) + sql + end + + def arel_attribute(name, table) + table[name] + end end def relation @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table, Post.predicate_builder end - (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method| + (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select, :left_joins]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal [:foo], relation.public_send("#{method}_values") @@ -55,9 +63,10 @@ module ActiveRecord test '#order! on non-string does not attempt regexp match for references' do obj = Object.new - obj.expects(:=~).never - assert relation.order!(obj) - assert_equal [obj], relation.order_values + assert_not_called(obj, :=~) do + assert relation.order!(obj) + assert_equal [obj], relation.order_values + end end test '#references!' do @@ -81,7 +90,7 @@ module ActiveRecord assert_equal [], relation.extending_values end - (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with, :uniq]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal :foo, relation.public_send("#{method}_value") @@ -153,13 +162,22 @@ module ActiveRecord test 'distinct!' do relation.distinct! :foo assert_equal :foo, relation.distinct_value - assert_equal :foo, relation.uniq_value # deprecated access + + assert_deprecated do + assert_equal :foo, relation.uniq_value # deprecated access + end end test 'uniq! was replaced by distinct!' do - relation.uniq! :foo + assert_deprecated(/use distinct! instead/) do + relation.uniq! :foo + end + + assert_deprecated(/use distinct_value instead/) do + assert_equal :foo, relation.uniq_value # deprecated access + end + assert_equal :foo, relation.distinct_value - assert_equal :foo, relation.uniq_value # deprecated access end end end diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb index 2006fc9611..ce8c5ca489 100644 --- a/activerecord/test/cases/relation/or_test.rb +++ b/activerecord/test/cases/relation/or_test.rb @@ -52,9 +52,11 @@ module ActiveRecord end def test_or_with_incompatible_relations - assert_raises ArgumentError do + error = assert_raises ArgumentError do Post.order('body asc').where('id = 1').or(Post.order('id desc').where(:id => [2, 3])).to_a end + + assert_equal "Relation passed to #or must be structurally compatible. Incompatible values: [:order]", error.message end def test_or_when_grouping @@ -80,5 +82,11 @@ module ActiveRecord assert_equal p.loaded?, true assert_equal expected, p.or(Post.where('id = 2')).to_a end + + def test_or_with_non_relation_object_raises_error + assert_raises ArgumentError do + Post.where(id: [1, 2, 3]).or(title: 'Rails') + end + end end end diff --git a/activerecord/test/cases/relation/record_fetch_warning_test.rb b/activerecord/test/cases/relation/record_fetch_warning_test.rb new file mode 100644 index 0000000000..0e0e23b24b --- /dev/null +++ b/activerecord/test/cases/relation/record_fetch_warning_test.rb @@ -0,0 +1,40 @@ +require 'cases/helper' +require 'models/post' +require 'active_record/relation/record_fetch_warning' + +module ActiveRecord + class RecordFetchWarningTest < ActiveRecord::TestCase + fixtures :posts + + def setup + @original_logger = ActiveRecord::Base.logger + @original_warn_on_records_fetched_greater_than = ActiveRecord::Base.warn_on_records_fetched_greater_than + @log = StringIO.new + end + + def teardown + ActiveRecord::Base.logger = @original_logger + ActiveRecord::Base.warn_on_records_fetched_greater_than = @original_warn_on_records_fetched_greater_than + end + + def test_warn_on_records_fetched_greater_than_allowed_limit + ActiveRecord::Base.logger = ActiveSupport::Logger.new(@log) + ActiveRecord::Base.logger.level = Logger::WARN + ActiveRecord::Base.warn_on_records_fetched_greater_than = 1 + + Post.all.to_a + + assert_match(/Query fetched/, @log.string) + end + + def test_does_not_warn_on_records_fetched_less_than_allowed_limit + ActiveRecord::Base.logger = ActiveSupport::Logger.new(@log) + ActiveRecord::Base.logger.level = Logger::WARN + ActiveRecord::Base.warn_on_records_fetched_greater_than = 100 + + Post.all.to_a + + assert_no_match(/Query fetched/, @log.string) + end + end +end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 6af31017d6..56a2b5b8c6 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require "models/author" require "models/binary" require "models/cake_designer" +require "models/car" require "models/chef" require "models/comment" require "models/edge" @@ -14,7 +15,7 @@ require "models/vertex" module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts, :edges, :authors, :binaries, :essays + fixtures :posts, :edges, :authors, :binaries, :essays, :cars, :treasures, :price_estimates def test_where_copies_bind_params author = authors(:david) @@ -114,6 +115,17 @@ module ActiveRecord assert_equal expected.to_sql, actual.to_sql end + def test_polymorphic_array_where_multiple_types + treasure_1 = treasures(:diamond) + treasure_2 = treasures(:sapphire) + car = cars(:honda) + + expected = [price_estimates(:diamond), price_estimates(:sapphire_1), price_estimates(:sapphire_2), price_estimates(:honda)].sort + actual = PriceEstimate.where(estimate_of: [treasure_1, treasure_2, car]).to_a.sort + + assert_equal expected, actual + end + def test_polymorphic_nested_relation_where expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: Treasure.where(id: [1,2])) actual = PriceEstimate.where(estimate_of: Treasure.where(id: [1,2])) @@ -276,5 +288,35 @@ module ActiveRecord assert_equal essays(:david_modest_proposal), essay end + + def test_where_with_strong_parameters + protected_params = Class.new do + attr_reader :permitted + alias :permitted? :permitted + + def initialize(parameters) + @parameters = parameters + @permitted = false + end + + def to_h + @parameters + end + + def permit! + @permitted = true + self + end + end + + author = authors(:david) + params = protected_params.new(name: author.name) + assert_raises(ActiveModel::ForbiddenAttributesError) { Author.where(params) } + assert_equal author, Author.where(params.permit!).first + end + + def test_where_with_unsupported_arguments + assert_raises(ArgumentError) { Author.where(42) } + end end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 9353be1ba7..03583344a8 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -20,6 +20,10 @@ module ActiveRecord def self.table_name 'fake_table' end + + def self.sanitize_sql_for_order(sql) + sql + end end def test_construction @@ -39,13 +43,17 @@ module ActiveRecord (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| assert_nil relation.send("#{method}_value"), method.to_s end - assert_equal({}, relation.create_with_value) + value = relation.create_with_value + assert_equal({}, value) + assert_predicate value, :frozen? end def test_multi_value_initialize relation = Relation.new(FakeKlass, :b, nil) Relation::MULTI_VALUE_METHODS.each do |method| - assert_equal [], relation.send("#{method}_values"), method.to_s + values = relation.send("#{method}_values") + assert_equal [], values, method.to_s + assert_predicate values, :frozen?, method.to_s end end @@ -57,9 +65,6 @@ module ActiveRecord def test_empty_where_values_hash relation = Relation.new(FakeKlass, :b, nil) assert_equal({}, relation.where_values_hash) - - relation.where! :hello - assert_equal({}, relation.where_values_hash) end def test_has_values @@ -76,7 +81,6 @@ module ActiveRecord def test_tree_is_not_traversed relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) - # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 left = relation.table[:id].eq(10) right = relation.table[:id].eq(10) combine = left.and right @@ -103,7 +107,6 @@ module ActiveRecord def test_create_with_value_with_wheres relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) - # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 relation.where! relation.table[:id].eq(10) relation.create_with_value = {:hello => 'world'} assert_equal({:hello => 'world', :id => 10}, relation.scope_for_create) @@ -114,7 +117,6 @@ module ActiveRecord relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) assert_equal({}, relation.scope_for_create) - # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 relation.where! relation.table[:id].eq(10) assert_equal({}, relation.scope_for_create) @@ -153,10 +155,10 @@ module ActiveRecord end test 'merging a hash into a relation' do - relation = Relation.new(FakeKlass, :b, nil) - relation = relation.merge where: :lol, readonly: true + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) + relation = relation.merge where: {name: :lol}, readonly: true - assert_equal Relation::WhereClause.new([:lol], []), relation.where_clause + assert_equal({"name"=>:lol}, relation.where_clause.to_h) assert_equal true, relation.readonly_value end @@ -185,7 +187,7 @@ module ActiveRecord end test '#values returns a dup of the values' do - relation = Relation.new(FakeKlass, :b, nil).where! :foo + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder).where!(name: :foo) values = relation.values values[:where] = nil @@ -234,6 +236,13 @@ module ActiveRecord assert_equal 3, relation.where(id: post.id).pluck(:id).size end + def test_merge_raises_with_invalid_argument + assert_raises ArgumentError do + relation = Relation.new(FakeKlass, :b, nil) + relation.merge(true) + end + end + def test_respond_to_for_non_selected_element post = Post.select(:title).first assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception" @@ -242,6 +251,24 @@ module ActiveRecord assert_equal false, post.respond_to?(:title), "post should not respond_to?(:body) since invoking it raises exception" end + def test_select_quotes_when_using_from_clause + skip_if_sqlite3_version_includes_quoting_bug + quoted_join = ActiveRecord::Base.connection.quote_table_name("join") + selected = Post.select(:join).from(Post.select("id as #{quoted_join}")).map(&:join) + assert_equal Post.pluck(:id), selected + end + + def test_selecting_aliased_attribute_quotes_column_name_when_from_is_used + skip_if_sqlite3_version_includes_quoting_bug + klass = Class.new(ActiveRecord::Base) do + self.table_name = :test_with_keyword_column_name + alias_attribute :description, :desc + end + klass.create!(description: "foo") + + assert_equal ["foo"], klass.select(:description).from(klass.all).map(&:desc) + end + def test_relation_merging_with_merged_joins_as_strings join_string = "LEFT OUTER JOIN #{Rating.quoted_table_name} ON #{SpecialComment.quoted_table_name}.id = #{Rating.quoted_table_name}.comment_id" special_comments_with_ratings = SpecialComment.joins join_string @@ -276,5 +303,26 @@ module ActiveRecord assert_equal "type cast from database", UpdateAllTestModel.first.body end + + private + + def skip_if_sqlite3_version_includes_quoting_bug + if sqlite3_version_includes_quoting_bug? + skip <<-ERROR.squish + You are using an outdated version of SQLite3 which has a bug in + quoted column names. Please update SQLite3 and rebuild the sqlite3 + ruby gem + ERROR + end + end + + def sqlite3_version_includes_quoting_bug? + if current_adapter?(:SQLite3Adapter) + selected_quoted_column_names = ActiveRecord::Base.connection.exec_query( + 'SELECT "join" FROM (SELECT id AS "join" FROM posts) subquery' + ).columns + ["join"] != selected_quoted_column_names + end + end end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 0cf44388fa..5604124bb3 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -17,7 +17,9 @@ require 'models/tyre' require 'models/minivan' require 'models/aircraft' require "models/possession" - +require "models/reader" +require "models/categorization" +require "models/edge" class RelationTest < ActiveRecord::TestCase fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments, @@ -110,15 +112,38 @@ class RelationTest < ActiveRecord::TestCase def test_loaded_first topics = Topic.all.order('id ASC') + topics.to_a # force load - assert_queries(1) do - topics.to_a # force load - 2.times { assert_equal "The First Topic", topics.first.title } + assert_no_queries do + assert_equal "The First Topic", topics.first.title + end + + assert topics.loaded? + end + + def test_loaded_first_with_limit + topics = Topic.all.order('id ASC') + topics.to_a # force load + + assert_no_queries do + assert_equal ["The First Topic", + "The Second Topic of the day"], topics.first(2).map(&:title) end assert topics.loaded? end + def test_first_get_more_than_available + topics = Topic.all.order('id ASC') + unloaded_first = topics.first(10) + topics.to_a # force load + + assert_no_queries do + loaded_first = topics.first(10) + assert_equal unloaded_first, loaded_first + end + end + def test_reload topics = Topic.all @@ -199,6 +224,39 @@ class RelationTest < ActiveRecord::TestCase assert_equal topics(:fifth).title, topics.first.title end + def test_reverse_order_with_function + topics = Topic.order("length(title)").reverse_order + assert_equal topics(:second).title, topics.first.title + end + + def test_reverse_order_with_function_other_predicates + topics = Topic.order("author_name, length(title), id").reverse_order + assert_equal topics(:second).title, topics.first.title + topics = Topic.order("length(author_name), id, length(title)").reverse_order + assert_equal topics(:fifth).title, topics.first.title + end + + def test_reverse_order_with_multiargument_function + assert_raises(ActiveRecord::IrreversibleOrderError) do + Topic.order("concat(author_name, title)").reverse_order + end + end + + def test_reverse_order_with_nulls_first_or_last + assert_raises(ActiveRecord::IrreversibleOrderError) do + Topic.order("title NULLS FIRST").reverse_order + end + assert_raises(ActiveRecord::IrreversibleOrderError) do + Topic.order("title nulls last").reverse_order + end + end + + def test_default_reverse_order_on_table_without_primary_key + assert_raises(ActiveRecord::IrreversibleOrderError) do + Edge.all.reverse_order + end + end + def test_order_with_hash_and_symbol_generates_the_same_sql assert_equal Topic.order(:id).to_sql, Topic.order(:id => :asc).to_sql end @@ -297,6 +355,17 @@ class RelationTest < ActiveRecord::TestCase assert_equal 3, tags.length end + def test_finding_with_sanitized_order + query = Tag.order(["field(id, ?)", [1,3,2]]).to_sql + assert_match(/field\(id, 1,3,2\)/, query) + + query = Tag.order(["field(id, ?)", []]).to_sql + assert_match(/field\(id, NULL\)/, query) + + query = Tag.order(["field(id, ?)", nil]).to_sql + assert_match(/field\(id, NULL\)/, query) + end + def test_finding_with_order_limit_and_offset entrants = Entrant.order("id ASC").limit(2).offset(1) @@ -621,6 +690,51 @@ class RelationTest < ActiveRecord::TestCase assert_equal 1, query.to_a.size end + def test_preloading_with_associations_and_merges + post = Post.create! title: 'Uhuu', body: 'body' + reader = Reader.create! post_id: post.id, person_id: 1 + comment = Comment.create! post_id: post.id, body: 'body' + + assert !comment.respond_to?(:readers) + + post_rel = Post.preload(:readers).joins(:readers).where(title: 'Uhuu') + result_comment = Comment.joins(:post).merge(post_rel).to_a.first + assert_equal comment, result_comment + + assert_no_queries do + assert_equal post, result_comment.post + assert_equal [reader], result_comment.post.readers.to_a + end + + post_rel = Post.includes(:readers).where(title: 'Uhuu') + result_comment = Comment.joins(:post).merge(post_rel).first + assert_equal comment, result_comment + + assert_no_queries do + assert_equal post, result_comment.post + assert_equal [reader], result_comment.post.readers.to_a + end + end + + def test_preloading_with_associations_default_scopes_and_merges + post = Post.create! title: 'Uhuu', body: 'body' + reader = Reader.create! post_id: post.id, person_id: 1 + + post_rel = PostWithPreloadDefaultScope.preload(:readers).joins(:readers).where(title: 'Uhuu') + result_post = PostWithPreloadDefaultScope.all.merge(post_rel).to_a.first + + assert_no_queries do + assert_equal [reader], result_post.readers.to_a + end + + post_rel = PostWithIncludesDefaultScope.includes(:readers).where(title: 'Uhuu') + result_post = PostWithIncludesDefaultScope.all.merge(post_rel).to_a.first + + assert_no_queries do + assert_equal [reader], result_post.readers.to_a + end + end + def test_loading_with_one_association posts = Post.preload(:comments) post = posts.find { |p| p.id == 1 } @@ -868,6 +982,12 @@ class RelationTest < ActiveRecord::TestCase assert authors.exists?(authors(:david).id) end + def test_any_with_scope_on_hash_includes + post = authors(:david).posts.first + categories = Categorization.includes(author: :posts).where(posts: { id: post.id }) + assert categories.exists? + end + def test_last authors = Author.all assert_equal authors(:bob), authors.last @@ -886,6 +1006,12 @@ class RelationTest < ActiveRecord::TestCase assert davids.loaded? end + def test_destroy_all_with_conditions_is_deprecated + assert_deprecated do + assert_difference('Author.count', -1) { Author.destroy_all(name: 'David') } + end + end + def test_delete_all davids = Author.where(:name => 'David') @@ -893,6 +1019,12 @@ class RelationTest < ActiveRecord::TestCase assert ! davids.loaded? end + def test_delete_all_with_conditions_is_deprecated + assert_deprecated do + assert_difference('Author.count', -1) { Author.delete_all(name: 'David') } + end + end + def test_delete_all_loaded davids = Author.where(:name => 'David') @@ -908,7 +1040,7 @@ class RelationTest < ActiveRecord::TestCase def test_delete_all_with_unpermitted_relation_raises_error assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all } - assert_raises(ActiveRecord::ActiveRecordError) { Author.uniq.delete_all } + assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all } assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all } assert_raises(ActiveRecord::ActiveRecordError) { Author.having('SUM(id) < 3').delete_all } assert_raises(ActiveRecord::ActiveRecordError) { Author.offset(10).delete_all } @@ -954,6 +1086,11 @@ class RelationTest < ActiveRecord::TestCase assert_equal 9, posts.where(:comments_count => 0).count end + def test_count_with_block + posts = Post.all + assert_equal 10, posts.count { |p| p.comments_count.even? } + end + def test_count_on_association_relation author = Author.last another_author = Author.first @@ -1147,6 +1284,16 @@ class RelationTest < ActiveRecord::TestCase assert posts.loaded? end + def test_to_a_should_dup_target + posts = Post.all + + original_size = posts.size + removed = posts.to_a.pop + + assert_equal original_size, posts.size + assert_includes posts.to_a, removed + end + def test_build posts = Post.all @@ -1484,6 +1631,13 @@ class RelationTest < ActiveRecord::TestCase assert_equal 'David', topic2.reload.author_name end + def test_update_on_relation_passing_active_record_object_is_deprecated + topic = Topic.create!(title: 'Foo', author_name: nil) + assert_deprecated(/update/) do + Topic.where(id: topic.id).update(topic, title: 'Bar') + end + end + def test_distinct tag1 = Tag.create(:name => 'Foo') tag2 = Tag.create(:name => 'Foo') @@ -1493,14 +1647,17 @@ class RelationTest < ActiveRecord::TestCase assert_equal ['Foo', 'Foo'], query.map(&:name) assert_sql(/DISTINCT/) do assert_equal ['Foo'], query.distinct.map(&:name) - assert_equal ['Foo'], query.uniq.map(&:name) + assert_deprecated { assert_equal ['Foo'], query.uniq.map(&:name) } end assert_sql(/DISTINCT/) do assert_equal ['Foo'], query.distinct(true).map(&:name) - assert_equal ['Foo'], query.uniq(true).map(&:name) + assert_deprecated { assert_equal ['Foo'], query.uniq(true).map(&:name) } end assert_equal ['Foo', 'Foo'], query.distinct(true).distinct(false).map(&:name) - assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name) + + assert_deprecated do + assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name) + end end def test_doesnt_add_having_values_if_options_are_blank @@ -1657,6 +1814,10 @@ class RelationTest < ActiveRecord::TestCase assert_sql(/^((?!ORDER).)*$/) { Post.all.find_by(author_id: 2) } end + test "find_by requires at least one argument" do + assert_raises(ArgumentError) { Post.all.find_by } + end + test "find_by! with hash conditions returns the first matching record" do assert_equal posts(:eager_other), Post.order(:id).find_by!(author_id: 2) end @@ -1679,6 +1840,10 @@ class RelationTest < ActiveRecord::TestCase end end + test "find_by! requires at least one argument" do + assert_raises(ArgumentError) { Post.all.find_by! } + end + test "loaded relations cannot be mutated by multi value methods" do relation = Post.all relation.to_a @@ -1829,4 +1994,22 @@ class RelationTest < ActiveRecord::TestCase def test_relation_join_method assert_equal 'Thank you for the welcome,Thank you again for the welcome', Post.first.comments.join(",") end + + def test_connection_adapters_can_reorder_binds + posts = Post.limit(1).offset(2) + + stubbed_connection = Post.connection.dup + def stubbed_connection.combine_bind_parameters(**kwargs) + offset = kwargs[:offset] + kwargs[:offset] = kwargs[:limit] + kwargs[:limit] = offset + super(**kwargs) + end + + posts.define_singleton_method(:connection) do + stubbed_connection + end + + assert_equal 2, posts.to_a.length + end end diff --git a/activerecord/test/cases/reload_models_test.rb b/activerecord/test/cases/reload_models_test.rb index 0d16a3526f..431fbf1297 100644 --- a/activerecord/test/cases/reload_models_test.rb +++ b/activerecord/test/cases/reload_models_test.rb @@ -3,7 +3,7 @@ require 'models/owner' require 'models/pet' class ReloadModelsTest < ActiveRecord::TestCase - fixtures :pets + fixtures :pets, :owners def test_has_one_with_reload pet = Pet.find_by_name('parrot') diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 262e0abc22..239f63d27b 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -9,11 +9,11 @@ class SanitizeTest < ActiveRecord::TestCase def test_sanitize_sql_array_handles_string_interpolation quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi") - assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi"]) - assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi".mb_chars]) + assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi"]) + assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi".mb_chars]) quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote_string("Bambi\nand\nThumper") - assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper"]) - assert_equal "name=#{quoted_bambi_and_thumper}",Binary.send(:sanitize_sql_array, ["name=%s", "Bambi\nand\nThumper".mb_chars]) + assert_equal "name='#{quoted_bambi_and_thumper}'",Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper"]) + assert_equal "name='#{quoted_bambi_and_thumper}'",Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper".mb_chars]) end def test_sanitize_sql_array_handles_bind_variables @@ -25,6 +25,16 @@ class SanitizeTest < ActiveRecord::TestCase assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars]) end + def test_sanitize_sql_array_handles_named_bind_variables + quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") + assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=:name", name: "Bambi"]) + assert_equal "name=#{quoted_bambi} AND id=1", Binary.send(:sanitize_sql_array, ["name=:name AND id=:id", name: "Bambi", id: 1]) + + quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") + assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=:name", name: "Bambi\nand\nThumper"]) + assert_equal "name=#{quoted_bambi_and_thumper} AND name2=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=:name AND name2=:name", name: "Bambi\nand\nThumper"]) + end + def test_sanitize_sql_array_handles_relations david = Author.create!(name: 'David') david_posts = david.posts.select(:id) @@ -69,4 +79,98 @@ class SanitizeTest < ActiveRecord::TestCase searchable_post.search("20% _reduction_!").to_a end end + + def test_bind_arity + assert_nothing_raised { bind '' } + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '', 1 } + + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?' } + assert_nothing_raised { bind '?', 1 } + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 } + end + + def test_named_bind_variables + assert_equal '1', bind(':a', :a => 1) # ' ruby-mode + assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode + + assert_nothing_raised { bind("'+00:00'", :foo => "bar") } + end + + def test_named_bind_arity + assert_nothing_raised { bind "name = :name", { name: "37signals" } } + assert_nothing_raised { bind "name = :name", { name: "37signals", id: 1 } } + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind "name = :name", { id: 1 } } + end + + class SimpleEnumerable + include Enumerable + + def initialize(ary) + @ary = ary + end + + def each(&b) + @ary.each(&b) + end + end + + def test_bind_enumerable + quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')}) + + assert_equal '1,2,3', bind('?', [1, 2, 3]) + assert_equal quoted_abc, bind('?', %w(a b c)) + + assert_equal '1,2,3', bind(':a', :a => [1, 2, 3]) + assert_equal quoted_abc, bind(':a', :a => %w(a b c)) # ' + + assert_equal '1,2,3', bind('?', SimpleEnumerable.new([1, 2, 3])) + assert_equal quoted_abc, bind('?', SimpleEnumerable.new(%w(a b c))) + + assert_equal '1,2,3', bind(':a', :a => SimpleEnumerable.new([1, 2, 3])) + assert_equal quoted_abc, bind(':a', :a => SimpleEnumerable.new(%w(a b c))) # ' + end + + def test_bind_empty_enumerable + quoted_nil = ActiveRecord::Base.connection.quote(nil) + assert_equal quoted_nil, bind('?', []) + assert_equal " in (#{quoted_nil})", bind(' in (?)', []) + assert_equal "foo in (#{quoted_nil})", bind('foo in (?)', []) + end + + def test_bind_empty_string + quoted_empty = ActiveRecord::Base.connection.quote('') + assert_equal quoted_empty, bind('?', '') + end + + def test_bind_chars + quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") + quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") + assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi") + assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper") + assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi".mb_chars) + assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper".mb_chars) + end + + def test_bind_record + o = Struct.new(:quoted_id).new(1) + assert_equal '1', bind('?', o) + + os = [o] * 3 + assert_equal '1,1,1', bind('?', os) + end + + def test_named_bind_with_postgresql_type_casts + l = Proc.new { bind(":a::integer '2009-01-01'::date", :a => '10') } + assert_nothing_raised(&l) + assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call + end + + private + def bind(statement, *vars) + if vars.first.is_a?(Hash) + ActiveRecord::Base.send(:replace_named_bind_variables, statement, vars.first) + else + ActiveRecord::Base.send(:replace_bind_variables, statement, vars) + end + end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 6bf4df70eb..f1927f561e 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -29,6 +29,24 @@ class SchemaDumperTest < ActiveRecord::TestCase ActiveRecord::SchemaMigration.delete_all end + if current_adapter?(:SQLite3Adapter) + %w{3.7.8 3.7.11 3.7.12}.each do |version_string| + test "dumps schema version for sqlite version #{version_string}" do + version = ActiveRecord::ConnectionAdapters::SQLite3Adapter::Version.new(version_string) + ActiveRecord::Base.connection.stubs(:sqlite_version).returns(version) + + versions = %w{ 20100101010101 20100201010101 20100301010101 } + versions.reverse_each do |v| + ActiveRecord::SchemaMigration.create!(:version => v) + end + + schema_info = ActiveRecord::Base.connection.dump_schema_information + assert_match(/20100201010101.*20100301010101/m, schema_info) + ActiveRecord::SchemaMigration.delete_all + end + end + end + def test_magic_comment assert_match "# encoding: #{Encoding.default_external.name}", standard_dump end @@ -38,6 +56,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{create_table "accounts"}, output assert_match %r{create_table "authors"}, output assert_no_match %r{create_table "schema_migrations"}, output + assert_no_match %r{create_table "ar_internal_metadata"}, output end def test_schema_dump_uses_force_cascade_on_create_table @@ -73,7 +92,7 @@ class SchemaDumperTest < ActiveRecord::TestCase next if column_set.empty? lengths = column_set.map do |column| - if match = column.match(/t\.(?:integer|decimal|float|datetime|timestamp|time|date|text|binary|string|boolean|xml|uuid|point)\s+"/) + if match = column.match(/\bt\.\w+\s+(?="\w+?")/) match[0].length end end.compact @@ -117,8 +136,8 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{c_int_4.*}, output assert_no_match %r{c_int_4.*limit:}, output - elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter) - assert_match %r{c_int_without_limit.*limit: 4}, output + elsif current_adapter?(:Mysql2Adapter) + assert_match %r{c_int_without_limit"$}, output assert_match %r{c_int_1.*limit: 1}, output assert_match %r{c_int_2.*limit: 2}, output @@ -146,10 +165,10 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{c_int_7.*limit: 7}, output assert_match %r{c_int_8.*limit: 8}, output else - assert_match %r{c_int_5.*limit: 8}, output - assert_match %r{c_int_6.*limit: 8}, output - assert_match %r{c_int_7.*limit: 8}, output - assert_match %r{c_int_8.*limit: 8}, output + assert_match %r{t\.bigint\s+"c_int_5"$}, output + assert_match %r{t\.bigint\s+"c_int_6"$}, output + assert_match %r{t\.bigint\s+"c_int_7"$}, output + assert_match %r{t\.bigint\s+"c_int_8"$}, output end end @@ -158,6 +177,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_no_match %r{create_table "accounts"}, output assert_match %r{create_table "authors"}, output assert_no_match %r{create_table "schema_migrations"}, output + assert_no_match %r{create_table "ar_internal_metadata"}, output end def test_schema_dump_with_regexp_ignored_table @@ -165,27 +185,28 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_no_match %r{create_table "accounts"}, output assert_match %r{create_table "authors"}, output assert_no_match %r{create_table "schema_migrations"}, output + assert_no_match %r{create_table "ar_internal_metadata"}, output end def test_schema_dumps_index_columns_in_right_order - index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip - if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) - assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition + index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_index/).first.strip + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition else - assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition end end def test_schema_dumps_partial_indices - index_definition = standard_dump.split(/\n/).grep(/add_index.*company_partial_index/).first.strip + index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_partial_index/).first.strip if current_adapter?(:PostgreSQLAdapter) - assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition - elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter) - assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition + elsif current_adapter?(:Mysql2Adapter) + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? - assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition else - assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index"', index_definition end end @@ -201,7 +222,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t\.boolean\s+"has_fun",.+default: false}, output end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_schema_dump_should_add_default_value_for_mysql_text_field output = standard_dump assert_match %r{t\.text\s+"body",\s+limit: 65535,\s+null: false$}, output @@ -215,7 +236,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_includes_length_for_mysql_blob_and_text_fields output = standard_dump - assert_match %r{t\.binary\s+"tiny_blob",\s+limit: 255$}, output + assert_match %r{t\.blob\s+"tiny_blob",\s+limit: 255$}, output assert_match %r{t\.binary\s+"normal_blob",\s+limit: 65535$}, output assert_match %r{t\.binary\s+"medium_blob",\s+limit: 16777215$}, output assert_match %r{t\.binary\s+"long_blob",\s+limit: 4294967295$}, output @@ -232,20 +253,35 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dumps_index_type output = standard_dump - assert_match %r{add_index "key_tests", \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output - assert_match %r{add_index "key_tests", \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output + assert_match %r{t\.index \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output + assert_match %r{t\.index \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output end end def test_schema_dump_includes_decimal_options output = dump_all_table_schema([/^[^n]/]) - assert_match %r{precision: 3,[[:space:]]+scale: 2,[[:space:]]+default: 2\.78}, output + assert_match %r{precision: 3,[[:space:]]+scale: 2,[[:space:]]+default: "2\.78"}, output end if current_adapter?(:PostgreSQLAdapter) def test_schema_dump_includes_bigint_default output = standard_dump - assert_match %r{t\.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output + assert_match %r{t\.bigint\s+"bigint_default",\s+default: 0}, output + end + + def test_schema_dump_includes_limit_on_array_type + output = standard_dump + assert_match %r{t\.bigint\s+"big_int_data_points\",\s+array: true}, output + end + + def test_schema_dump_allows_array_of_decimal_defaults + output = standard_dump + assert_match %r{t\.decimal\s+"decimal_array_default",\s+default: \["1.23", "3.45"\],\s+array: true}, output + end + + def test_schema_dump_expression_indices + index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_expression_index/).first.strip + assert_equal 't.index "lower((name)::text)", name: "company_expression_index", using: :btree', index_definition end if ActiveRecord::Base.connection.supports_extensions? @@ -302,7 +338,7 @@ class SchemaDumperTest < ActiveRecord::TestCase end end - class CreateDogMigration < ActiveRecord::Migration + class CreateDogMigration < ActiveRecord::Migration::Current def up create_table("dog_owners") do |t| end @@ -310,9 +346,9 @@ class SchemaDumperTest < ActiveRecord::TestCase create_table("dogs") do |t| t.column :name, :string t.column :owner_id, :integer + t.index [:name] + t.foreign_key :dog_owners, column: "owner_id" if supports_foreign_keys? end - add_index "dogs", [:name] - add_foreign_key :dogs, :dog_owners, column: "owner_id" if supports_foreign_keys? end def down drop_table("dogs") @@ -332,6 +368,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_no_match %r{create_table "foo_.+_bar"}, output assert_no_match %r{add_index "foo_.+_bar"}, output assert_no_match %r{create_table "schema_migrations"}, output + assert_no_match %r{create_table "ar_internal_metadata"}, output if ActiveRecord::Base.connection.supports_foreign_keys? assert_no_match %r{add_foreign_key "foo_.+_bar"}, output @@ -343,6 +380,38 @@ class SchemaDumperTest < ActiveRecord::TestCase ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = '' $stdout = original end + + def test_schema_dump_with_table_name_prefix_and_ignoring_tables + original, $stdout = $stdout, StringIO.new + + create_cat_migration = Class.new(ActiveRecord::Migration::Current) do + def change + create_table("cats") do |t| + end + create_table("omg_cats") do |t| + end + end + end + + original_table_name_prefix = ActiveRecord::Base.table_name_prefix + original_schema_dumper_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + ActiveRecord::Base.table_name_prefix = 'omg_' + ActiveRecord::SchemaDumper.ignore_tables = ["cats"] + migration = create_cat_migration.new + migration.migrate(:up) + + stream = StringIO.new + output = ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream).string + + assert_match %r{create_table "omg_cats"}, output + refute_match %r{create_table "cats"}, output + ensure + migration.migrate(:down) + ActiveRecord::Base.table_name_prefix = original_table_name_prefix + ActiveRecord::SchemaDumper.ignore_tables = original_schema_dumper_ignore_tables + + $stdout = original + end end class SchemaDumperDefaultsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/schema_loading_test.rb b/activerecord/test/cases/schema_loading_test.rb new file mode 100644 index 0000000000..3d92a5e104 --- /dev/null +++ b/activerecord/test/cases/schema_loading_test.rb @@ -0,0 +1,52 @@ +require "cases/helper" + +module SchemaLoadCounter + extend ActiveSupport::Concern + + module ClassMethods + attr_accessor :load_schema_calls + + def load_schema! + self.load_schema_calls ||= 0 + self.load_schema_calls +=1 + super + end + end +end + +class SchemaLoadingTest < ActiveRecord::TestCase + def test_basic_model_is_loaded_once + klass = define_model + klass.new + assert_equal 1, klass.load_schema_calls + end + + def test_model_with_custom_lock_is_loaded_once + klass = define_model do |c| + c.table_name = :lock_without_defaults_cust + c.locking_column = :custom_lock_version + end + klass.new + assert_equal 1, klass.load_schema_calls + end + + def test_model_with_changed_custom_lock_is_loaded_twice + klass = define_model do |c| + c.table_name = :lock_without_defaults_cust + end + klass.new + klass.locking_column = :custom_lock_version + klass.new + assert_equal 2, klass.load_schema_calls + end + + private + + def define_model + Class.new(ActiveRecord::Base) do + include SchemaLoadCounter + self.table_name = :lock_without_defaults + yield self if block_given? + end + end +end diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 4137b20c4a..dcd09b6973 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -3,6 +3,8 @@ require 'models/post' require 'models/comment' require 'models/developer' require 'models/computer' +require 'models/vehicle' +require 'models/cat' class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts, :comments @@ -153,6 +155,18 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected_7, received_7 end + def test_unscope_comparison_where_clauses + # unscoped for WHERE (`developers`.`id` <= 2) + expected = Developer.order('salary DESC').collect(&:name) + received = DeveloperOrderedBySalary.where(id: -Float::INFINITY..2).unscope(where: :id).collect { |dev| dev.name } + assert_equal expected, received + + # unscoped for WHERE (`developers`.`id` < 2) + expected = Developer.order('salary DESC').collect(&:name) + received = DeveloperOrderedBySalary.where(id: -Float::INFINITY...2).unscope(where: :id).collect { |dev| dev.name } + assert_equal expected, received + end + def test_unscope_multiple_where_clauses expected = Developer.order('salary DESC').collect(&:name) received = DeveloperOrderedBySalary.where(name: 'Jamis').where(id: 1).unscope(where: [:name, :id]).collect(&:name) @@ -361,6 +375,18 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 10, DeveloperCalledJamis.unscoped { DeveloperCalledJamis.poor }.length end + def test_default_scope_with_joins + assert_equal Comment.where(post_id: SpecialPostWithDefaultScope.pluck(:id)).count, + Comment.joins(:special_post_with_default_scope).count + assert_equal Comment.where(post_id: Post.pluck(:id)).count, + Comment.joins(:post).count + end + + def test_unscoped_with_joins_should_not_have_default_scope + assert_equal SpecialPostWithDefaultScope.unscoped { Comment.joins(:special_post_with_default_scope).to_a }, + Comment.joins(:post).to_a + end + def test_default_scope_select_ignored_by_aggregations assert_equal DeveloperWithSelect.all.to_a.count, DeveloperWithSelect.count end @@ -441,4 +467,34 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 1, scope.where_clause.ast.children.length assert_equal Developer.where(name: "David"), scope end + + def test_with_abstract_class_where_clause_should_not_be_duplicated + scope = Bus.all + assert_equal scope.where_clause.ast.children.length, 1 + end + + def test_sti_conditions_are_not_carried_in_default_scope + ConditionalStiPost.create! body: '' + SubConditionalStiPost.create! body: '' + SubConditionalStiPost.create! title: 'Hello world', body: '' + + assert_equal 2, ConditionalStiPost.count + assert_equal 2, ConditionalStiPost.all.to_a.size + assert_equal 3, ConditionalStiPost.unscope(where: :title).to_a.size + + assert_equal 1, SubConditionalStiPost.count + assert_equal 1, SubConditionalStiPost.all.to_a.size + assert_equal 2, SubConditionalStiPost.unscope(where: :title).to_a.size + end + + def test_with_abstract_class_scope_should_be_executed_in_correct_context + vegetarian_pattern, gender_pattern = if current_adapter?(:Mysql2Adapter) + [/`lions`.`is_vegetarian`/, /`lions`.`gender`/] + else + [/"lions"."is_vegetarian"/, /"lions"."gender"/] + end + + assert_match vegetarian_pattern, Lion.all.to_sql + assert_match gender_pattern, Lion.female.to_sql + end end diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index e4cc533517..0e277ed235 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -188,8 +188,9 @@ class NamedScopingTest < ActiveRecord::TestCase def test_any_should_call_proxy_found_if_using_a_block topics = Topic.base assert_queries(1) do - topics.expects(:empty?).never - topics.any? { true } + assert_not_called(topics, :empty?) do + topics.any? { true } + end end end @@ -217,8 +218,9 @@ class NamedScopingTest < ActiveRecord::TestCase def test_many_should_call_proxy_found_if_using_a_block topics = Topic.base assert_queries(1) do - topics.expects(:size).never - topics.many? { true } + assert_not_called(topics, :size) do + topics.many? { true } + end end end @@ -299,7 +301,7 @@ class NamedScopingTest < ActiveRecord::TestCase :relation, # private class method on AR::Base :new, # redefined class method on AR::Base :all, # a default scope - :public, # some imporant methods on Module and Class + :public, # some important methods on Module and Class :protected, :private, :name, @@ -438,6 +440,25 @@ class NamedScopingTest < ActiveRecord::TestCase end end + def test_scopes_with_reserved_names + class << Topic + def public_method; end + public :public_method + + def protected_method; end + protected :protected_method + + def private_method; end + private :private_method + end + + [:public_method, :protected_method, :private_method].each do |reserved_method| + assert Topic.respond_to?(reserved_method, true) + ActiveRecord::Base.logger.expects(:warn) + silence_warnings { Topic.scope(reserved_method, -> { }) } + end + end + def test_scopes_on_relations # Topic.replied approved_topics = Topic.all.approved.order('id DESC') @@ -523,4 +544,18 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal 1, SpecialComment.where(body: 'go crazy').created.count end + def test_model_class_should_respond_to_none + assert !Topic.none? + Topic.delete_all + assert Topic.none? + end + + def test_model_class_should_respond_to_one + assert !Topic.one? + Topic.delete_all + assert !Topic.one? + Topic.create! + assert Topic.one? + end + end diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 4bfffbe9c6..c15d57460b 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -209,9 +209,23 @@ class RelationScopingTest < ActiveRecord::TestCase assert_not_equal [], Developer.all end - def test_current_scope_does_not_pollute_other_subclasses - Post.none.scoping do - assert StiPost.all.any? + def test_current_scope_does_not_pollute_sibling_subclasses + Comment.none.scoping do + assert_not SpecialComment.all.any? + assert_not VerySpecialComment.all.any? + assert_not SubSpecialComment.all.any? + end + + SpecialComment.none.scoping do + assert Comment.all.any? + assert VerySpecialComment.all.any? + assert_not SubSpecialComment.all.any? + end + + SubSpecialComment.none.scoping do + assert Comment.all.any? + assert VerySpecialComment.all.any? + assert SpecialComment.all.any? end end end diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 35b13ea247..14b80f4df4 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -8,7 +8,7 @@ require 'models/post' class SerializationTest < ActiveRecord::TestCase fixtures :books - FORMATS = [ :xml, :json ] + FORMATS = [ :json ] def setup @contact_attributes = { diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index e29f7462c8..846be857d0 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -264,4 +264,68 @@ class SerializedAttributeTest < ActiveRecord::TestCase Topic.serialize(:content, Regexp) end end + + def test_newly_emptied_serialized_hash_is_changed + Topic.serialize(:content, Hash) + topic = Topic.create(content: { "things" => "stuff" }) + topic.content.delete("things") + topic.save! + topic.reload + + assert_equal({}, topic.content) + end + + def test_values_cast_from_nil_are_persisted_as_nil + # This is required to fulfil the following contract, which must be universally + # true in Active Record: + # + # model.attribute = value + # assert_equal model.attribute, model.tap(&:save).reload.attribute + Topic.serialize(:content, Hash) + topic = Topic.create!(content: {}) + topic2 = Topic.create!(content: nil) + + assert_equal [topic, topic2], Topic.where(content: nil) + end + + def test_nil_is_always_persisted_as_null + Topic.serialize(:content, Hash) + + topic = Topic.create!(content: { foo: "bar" }) + topic.update_attribute :content, nil + assert_equal [topic], Topic.where(content: nil) + end + + def test_mutation_detection_does_not_double_serialize + coder = Object.new + def coder.dump(value) + return if value.nil? + value + " encoded" + end + def coder.load(value) + return if value.nil? + value.gsub(" encoded", "") + end + type = Class.new(ActiveModel::Type::Value) do + include ActiveModel::Type::Helpers::Mutable + + def serialize(value) + return if value.nil? + value + " serialized" + end + + def deserialize(value) + return if value.nil? + value.gsub(" serialized", "") + end + end.new + model = Class.new(Topic) do + attribute :foo, type + serialize :foo, coder + end + + topic = model.create!(foo: "bar") + topic.foo + refute topic.changed? + end end diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb index a704b861cb..104226010a 100644 --- a/activerecord/test/cases/statement_cache_test.rb +++ b/activerecord/test/cases/statement_cache_test.rb @@ -94,5 +94,17 @@ module ActiveRecord additional_books = cache.execute([], Book, Book.connection) assert first_books != additional_books end + + def test_unprepared_statements_dont_share_a_cache_with_prepared_statements + Book.create(name: "my book") + Book.create(name: "my other book") + + book = Book.find_by(name: "my book") + other_book = Book.connection.unprepared_statement do + Book.find_by(name: "my other book") + end + + refute_equal book, other_book + end end end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index e9cdf94c99..bce86875e1 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -104,7 +104,7 @@ class StoreTest < ActiveRecord::TestCase assert_equal true, user.settings.instance_of?(ActiveSupport::HashWithIndifferentAccess) end - test "convert store attributes from any format other than Hash or HashWithIndifferent access losing the data" do + test "convert store attributes from any format other than Hash or HashWithIndifferentAccess losing the data" do @john.json_data = "somedata" @john.height = 'low' assert_equal true, @john.json_data.instance_of?(ActiveSupport::HashWithIndifferentAccess) @@ -177,6 +177,7 @@ class StoreTest < ActiveRecord::TestCase assert_equal [:color], first_model.stored_attributes[:data] assert_equal [:color, :width, :height], second_model.stored_attributes[:data] assert_equal [:color, :area, :volume], third_model.stored_attributes[:data] + assert_equal [:color], first_model.stored_attributes[:data] end test "YAML coder initializes the store when a Nil value is given" do diff --git a/activerecord/test/cases/suppressor_test.rb b/activerecord/test/cases/suppressor_test.rb index 1c449d42fe..2f00241de2 100644 --- a/activerecord/test/cases/suppressor_test.rb +++ b/activerecord/test/cases/suppressor_test.rb @@ -3,7 +3,38 @@ require 'models/notification' require 'models/user' class SuppressorTest < ActiveRecord::TestCase - def test_suppresses_creation_of_record_generated_by_callback + def test_suppresses_create + assert_no_difference -> { Notification.count } do + Notification.suppress do + Notification.create + Notification.create! + Notification.new.save + Notification.new.save! + end + end + end + + def test_suppresses_update + user = User.create! token: 'asdf' + + User.suppress do + user.update token: 'ghjkl' + assert_equal 'asdf', user.reload.token + + user.update! token: 'zxcvbnm' + assert_equal 'asdf', user.reload.token + + user.token = 'qwerty' + user.save + assert_equal 'asdf', user.reload.token + + user.token = 'uiop' + user.save! + assert_equal 'asdf', user.reload.token + end + end + + def test_suppresses_create_in_callback assert_difference -> { User.count } do assert_no_difference -> { Notification.count } do Notification.suppress { UserWithNotification.create! } @@ -15,7 +46,30 @@ class SuppressorTest < ActiveRecord::TestCase Notification.suppress { UserWithNotification.create! } assert_difference -> { Notification.count } do - Notification.create! + Notification.create!(message: "New Comment") + end + end + + def test_suppresses_validations_on_create + assert_no_difference -> { Notification.count } do + Notification.suppress do + User.create + User.create! + User.new.save + User.new.save! + end + end + end + + def test_suppresses_when_nested_multiple_times + assert_no_difference -> { Notification.count } do + Notification.suppress do + Notification.suppress { } + Notification.create + Notification.create! + Notification.new.save + Notification.new.save! + end end end end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index 38164b2228..510bb088c8 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -8,16 +8,50 @@ module ActiveRecord ActiveRecord::Tasks::MySQLDatabaseTasks.stubs(:new).returns @mysql_tasks ActiveRecord::Tasks::PostgreSQLDatabaseTasks.stubs(:new).returns @postgresql_tasks ActiveRecord::Tasks::SQLiteDatabaseTasks.stubs(:new).returns @sqlite_tasks + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end end ADAPTERS_TASKS = { - mysql: :mysql_tasks, mysql2: :mysql_tasks, postgresql: :postgresql_tasks, sqlite3: :sqlite_tasks } + class DatabaseTasksUtilsTask< ActiveRecord::TestCase + def test_raises_an_error_when_called_with_protected_environment + ActiveRecord::Migrator.stubs(:current_version).returns(1) + + protected_environments = ActiveRecord::Base.protected_environments.dup + current_env = ActiveRecord::Migrator.current_environment + assert !protected_environments.include?(current_env) + # Assert no error + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + + ActiveRecord::Base.protected_environments << current_env + assert_raise(ActiveRecord::ProtectedEnvironmentError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end + ensure + ActiveRecord::Base.protected_environments = protected_environments + end + + def test_raises_an_error_if_no_migrations_have_been_made + ActiveRecord::InternalMetadata.stubs(:table_exists?).returns(false) + ActiveRecord::Migrator.stubs(:current_version).returns(1) + + assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end + end + end + class DatabaseTasksRegisterTask < ActiveRecord::TestCase def test_register_task klazz = Class.new do @@ -134,21 +168,25 @@ module ActiveRecord with('database' => 'dev-db') ActiveRecord::Tasks::DatabaseTasks.expects(:create). with('database' => 'test-db') - ENV.expects(:[]).with('RAILS_ENV').returns(nil) ActiveRecord::Tasks::DatabaseTasks.create_current( ActiveSupport::StringInquirer.new('development') ) end - def test_creates_only_development_database_when_rails_env_is_development + def test_creates_test_and_development_databases_when_rails_env_is_development + old_env = ENV['RAILS_ENV'] + ENV['RAILS_ENV'] = 'development' ActiveRecord::Tasks::DatabaseTasks.expects(:create). with('database' => 'dev-db') - ENV.expects(:[]).with('RAILS_ENV').returns('development') + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with('database' => 'test-db') ActiveRecord::Tasks::DatabaseTasks.create_current( ActiveSupport::StringInquirer.new('development') ) + ensure + ENV['RAILS_ENV'] = old_env end def test_establishes_connection_for_the_given_environment @@ -255,36 +293,55 @@ module ActiveRecord with('database' => 'dev-db') ActiveRecord::Tasks::DatabaseTasks.expects(:drop). with('database' => 'test-db') - ENV.expects(:[]).with('RAILS_ENV').returns(nil) ActiveRecord::Tasks::DatabaseTasks.drop_current( ActiveSupport::StringInquirer.new('development') ) end - def test_drops_only_development_database_when_rails_env_is_development + def test_drops_testand_development_databases_when_rails_env_is_development + old_env = ENV['RAILS_ENV'] + ENV['RAILS_ENV'] = 'development' ActiveRecord::Tasks::DatabaseTasks.expects(:drop). with('database' => 'dev-db') - ENV.expects(:[]).with('RAILS_ENV').returns('development') + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with('database' => 'test-db') ActiveRecord::Tasks::DatabaseTasks.drop_current( ActiveSupport::StringInquirer.new('development') ) + ensure + ENV['RAILS_ENV'] = old_env end end class DatabaseTasksMigrateTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + def setup + ActiveRecord::Tasks::DatabaseTasks.migrations_paths = 'custom/path' + end + + def teardown + ActiveRecord::Tasks::DatabaseTasks.migrations_paths = nil + end + def test_migrate_receives_correct_env_vars verbose, version = ENV['VERBOSE'], ENV['VERSION'] ENV['VERBOSE'] = 'false' ENV['VERSION'] = '4' - ActiveRecord::Migrator.expects(:migrate).with(ActiveRecord::Migrator.migrations_paths, 4) + ActiveRecord::Migrator.expects(:migrate).with('custom/path', 4) ActiveRecord::Tasks::DatabaseTasks.migrate ensure ENV['VERBOSE'], ENV['VERSION'] = verbose, version end + + def test_migrate_clears_schema_cache_afterward + ActiveRecord::Base.expects(:clear_cache!) + ActiveRecord::Tasks::DatabaseTasks.migrate + end end class DatabaseTasksPurgeTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index f58535f044..8e480bbaee 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -1,48 +1,49 @@ require 'cases/helper' +require 'active_record/tasks/database_tasks' -if current_adapter?(:MysqlAdapter, :Mysql2Adapter) +if current_adapter?(:Mysql2Adapter) module ActiveRecord class MysqlDBCreateTest < ActiveRecord::TestCase def setup @connection = stub(:create_database => true) @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'my-app-db' } ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end def test_establishes_connection_without_database ActiveRecord::Base.expects(:establish_connection). - with('adapter' => 'mysql', 'database' => nil) + with('adapter' => 'mysql2', 'database' => nil) ActiveRecord::Tasks::DatabaseTasks.create @configuration end - def test_creates_database_with_default_encoding_and_collation + def test_creates_database_with_no_default_options @connection.expects(:create_database). - with('my-app-db', charset: 'utf8', collation: 'utf8_unicode_ci') + with('my-app-db', {}) ActiveRecord::Tasks::DatabaseTasks.create @configuration end - def test_creates_database_with_given_encoding_and_default_collation - @connection.expects(:create_database). - with('my-app-db', charset: 'utf8', collation: 'utf8_unicode_ci') - - ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge('encoding' => 'utf8') - end - - def test_creates_database_with_given_encoding_and_no_collation + def test_creates_database_with_given_encoding @connection.expects(:create_database). with('my-app-db', charset: 'latin1') ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge('encoding' => 'latin1') end - def test_creates_database_with_given_collation_and_no_encoding + def test_creates_database_with_given_collation @connection.expects(:create_database). with('my-app-db', collation: 'latin1_swedish_ci') @@ -55,108 +56,118 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.create @configuration end - def test_create_when_database_exists_outputs_info_to_stderr - $stderr.expects(:puts).with("my-app-db already exists").once + def test_when_database_created_successfully_outputs_info_to_stdout + ActiveRecord::Tasks::DatabaseTasks.create @configuration + assert_equal $stdout.string, "Created database 'my-app-db'\n" + end + + def test_create_when_database_exists_outputs_info_to_stderr ActiveRecord::Base.connection.stubs(:create_database).raises( - ActiveRecord::StatementInvalid.new("Can't create database 'dev'; database exists:") + ActiveRecord::Tasks::DatabaseAlreadyExists ) ActiveRecord::Tasks::DatabaseTasks.create @configuration + + assert_equal $stderr.string, "Database 'my-app-db' already exists\n" end end - if current_adapter?(:MysqlAdapter) - class MysqlDBCreateAsRootTest < ActiveRecord::TestCase - def setup - @connection = stub("Connection", create_database: true) - @error = Mysql::Error.new "Invalid permissions" - @configuration = { - 'adapter' => 'mysql', - 'database' => 'my-app-db', - 'username' => 'pat', - 'password' => 'wossname' - } - - $stdin.stubs(:gets).returns("secret\n") - $stdout.stubs(:print).returns(nil) - @error.stubs(:errno).returns(1045) - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection). - raises(@error). - then.returns(true) - end + class MysqlDBCreateAsRootTest < ActiveRecord::TestCase + def setup + @connection = stub("Connection", create_database: true) + @error = Mysql2::Error.new("Invalid permissions") + @configuration = { + 'adapter' => 'mysql2', + 'database' => 'my-app-db', + 'username' => 'pat', + 'password' => 'wossname' + } - if defined?(::Mysql) - def test_root_password_is_requested - assert_permissions_granted_for "pat" - $stdin.expects(:gets).returns("secret\n") + $stdin.stubs(:gets).returns("secret\n") + $stdout.stubs(:print).returns(nil) + @error.stubs(:errno).returns(1045) + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection). + raises(@error). + then.returns(true) - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end - end + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end - def test_connection_established_as_root - assert_permissions_granted_for "pat" - ActiveRecord::Base.expects(:establish_connection).with( - 'adapter' => 'mysql', - 'database' => nil, - 'username' => 'root', - 'password' => 'secret' - ) + def teardown + $stdout, $stderr = @original_stdout, @original_stderr + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_root_password_is_requested + assert_permissions_granted_for("pat") + $stdin.expects(:gets).returns("secret\n") - def test_database_created_by_root - assert_permissions_granted_for "pat" - @connection.expects(:create_database). - with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci') + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_connection_established_as_root + assert_permissions_granted_for("pat") + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'mysql2', + 'database' => nil, + 'username' => 'root', + 'password' => 'secret' + ) - def test_grant_privileges_for_normal_user - assert_permissions_granted_for "pat" - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - def test_do_not_grant_privileges_for_root_user - @configuration['username'] = 'root' - @configuration['password'] = '' - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_database_created_by_root + assert_permissions_granted_for("pat") + @connection.expects(:create_database). + with('my-app-db', {}) - def test_connection_established_as_normal_user - assert_permissions_granted_for "pat" - ActiveRecord::Base.expects(:establish_connection).returns do - ActiveRecord::Base.expects(:establish_connection).with( - 'adapter' => 'mysql', - 'database' => 'my-app-db', - 'username' => 'pat', - 'password' => 'secret' - ) + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - raise @error - end + def test_grant_privileges_for_normal_user + assert_permissions_granted_for("pat") + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_do_not_grant_privileges_for_root_user + @configuration['username'] = 'root' + @configuration['password'] = '' + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_connection_established_as_normal_user + assert_permissions_granted_for("pat") + ActiveRecord::Base.expects(:establish_connection).returns do + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'mysql2', + 'database' => 'my-app-db', + 'username' => 'pat', + 'password' => 'secret' + ) - ActiveRecord::Tasks::DatabaseTasks.create @configuration + raise @error end - def test_sends_output_to_stderr_when_other_errors - @error.stubs(:errno).returns(42) + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - $stderr.expects(:puts).at_least_once.returns(nil) + def test_sends_output_to_stderr_when_other_errors + @error.stubs(:errno).returns(42) - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + $stderr.expects(:puts).at_least_once.returns(nil) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - private - def assert_permissions_granted_for(db_user) - db_name = @configuration['database'] - db_password = @configuration['password'] - @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") - end + private + + def assert_permissions_granted_for(db_user) + db_name = @configuration['database'] + db_password = @configuration['password'] + @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") end end @@ -164,12 +175,19 @@ module ActiveRecord def setup @connection = stub(:drop_database => true) @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'my-app-db' } ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end def test_establishes_connection_to_mysql_database @@ -183,13 +201,19 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop @configuration end + + def test_when_database_dropped_successfully_outputs_info_to_stdout + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + + assert_equal $stdout.string, "Dropped database 'my-app-db'\n" + end end class MySQLPurgeTest < ActiveRecord::TestCase def setup @connection = stub(:recreate_database => true) @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'test-db' } @@ -203,9 +227,9 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.purge @configuration end - def test_recreates_database_with_the_default_options + def test_recreates_database_with_no_default_options @connection.expects(:recreate_database). - with('test-db', charset: 'utf8', collation: 'utf8_unicode_ci') + with('test-db', {}) ActiveRecord::Tasks::DatabaseTasks.purge @configuration end @@ -223,7 +247,7 @@ module ActiveRecord def setup @connection = stub(:create_database => true) @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'my-app-db' } @@ -241,7 +265,7 @@ module ActiveRecord def setup @connection = stub(:create_database => true) @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'my-app-db' } @@ -258,43 +282,53 @@ module ActiveRecord class MySQLStructureDumpTest < ActiveRecord::TestCase def setup @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'test-db' } end def test_structure_dump filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db").returns(true) + Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) end - def test_warn_when_external_structure_dump_fails + def test_warn_when_external_structure_dump_command_execution_fails filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db").returns(false) + Kernel.expects(:system) + .with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db") + .returns(false) - warnings = capture(:stderr) do + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) - end - - assert_match(/Could not dump the database structure/, warnings) + } + assert_match(/^failed to execute: `mysqldump`$/, e.message) end def test_structure_dump_with_port_number filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--port", "10000", "--result-file", filename, "--no-data", "test-db").returns(true) + Kernel.expects(:system).with("mysqldump", "--port=10000", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump( @configuration.merge('port' => 10000), filename) end + + def test_structure_dump_with_ssl + filename = "awesome-file.sql" + Kernel.expects(:system).with("mysqldump", "--ssl-ca=ca.crt", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump( + @configuration.merge("sslca" => "ca.crt"), + filename) + end end class MySQLStructureLoadTest < ActiveRecord::TestCase def setup @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'test-db' } end @@ -302,10 +336,10 @@ module ActiveRecord def test_structure_load filename = "awesome-file.sql" Kernel.expects(:system).with('mysql', '--execute', %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db") + .returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end end - end end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index d45fb07417..6a0c7fbcb5 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -1,4 +1,5 @@ require 'cases/helper' +require 'active_record/tasks/database_tasks' if current_adapter?(:PostgreSQLAdapter) module ActiveRecord @@ -12,6 +13,13 @@ module ActiveRecord ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end def test_establishes_connection_to_postgresql_database @@ -60,17 +68,23 @@ module ActiveRecord $stderr.expects(:puts). with("Couldn't create database for #{@configuration.inspect}") + assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration } + end + + def test_when_database_created_successfully_outputs_info_to_stdout ActiveRecord::Tasks::DatabaseTasks.create @configuration + + assert_equal $stdout.string, "Created database 'my-app-db'\n" end def test_create_when_database_exists_outputs_info_to_stderr - $stderr.expects(:puts).with("my-app-db already exists").once - ActiveRecord::Base.connection.stubs(:create_database).raises( - ActiveRecord::StatementInvalid.new('database "my-app-db" already exists') + ActiveRecord::Tasks::DatabaseAlreadyExists ) ActiveRecord::Tasks::DatabaseTasks.create @configuration + + assert_equal $stderr.string, "Database 'my-app-db' already exists\n" end end @@ -84,6 +98,13 @@ module ActiveRecord ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end def test_establishes_connection_to_postgresql_database @@ -101,6 +122,12 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop @configuration end + + def test_when_database_dropped_successfully_outputs_info_to_stdout + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + + assert_equal $stdout.string, "Dropped database 'my-app-db'\n" + end end class PostgreSQLPurgeTest < ActiveRecord::TestCase @@ -204,7 +231,7 @@ module ActiveRecord end def test_structure_dump - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} my-app-db").returns(true) + Kernel.expects(:system).with('pg_dump', '-s', '-x', '-O', '-f', @filename, 'my-app-db').returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) end @@ -212,7 +239,7 @@ module ActiveRecord def test_structure_dump_with_schema_search_path @configuration['schema_search_path'] = 'foo,bar' - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} --schema=foo --schema=bar my-app-db").returns(true) + Kernel.expects(:system).with('pg_dump', '-s', '-x', '-O', '-f', @filename, '--schema=foo', '--schema=bar', 'my-app-db').returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) end @@ -220,7 +247,7 @@ module ActiveRecord def test_structure_dump_with_schema_search_path_and_dump_schemas_all @configuration['schema_search_path'] = 'foo,bar' - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} my-app-db").returns(true) + Kernel.expects(:system).with("pg_dump", '-s', '-x', '-O', '-f', @filename, 'my-app-db').returns(true) with_dump_schemas(:all) do ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) @@ -228,7 +255,7 @@ module ActiveRecord end def test_structure_dump_with_dump_schemas_string - Kernel.expects(:system).with("pg_dump -i -s -x -O -f #{@filename} --schema=foo --schema=bar my-app-db").returns(true) + Kernel.expects(:system).with("pg_dump", '-s', '-x', '-O', '-f', @filename, '--schema=foo', '--schema=bar', "my-app-db").returns(true) with_dump_schemas('foo,bar') do ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) @@ -261,18 +288,17 @@ module ActiveRecord def test_structure_load filename = "awesome-file.sql" - Kernel.expects(:system).with("psql -q -f #{filename} my-app-db") + Kernel.expects(:system).with('psql', '-q', '-f', filename, @configuration['database']).returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end def test_structure_load_accepts_path_with_spaces filename = "awesome file.sql" - Kernel.expects(:system).with("psql -q -f awesome\\ file.sql my-app-db") + Kernel.expects(:system).with('psql', '-q', '-f', filename, @configuration['database']).returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end end - end end diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index 750d5e42dc..4be03c7f61 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -1,4 +1,5 @@ require 'cases/helper' +require 'active_record/tasks/database_tasks' require 'pathname' if current_adapter?(:SQLite3Adapter) @@ -15,6 +16,13 @@ module ActiveRecord File.stubs(:exist?).returns(false) ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end def test_db_checks_database_exists @@ -23,12 +31,18 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' end + def test_when_db_created_successfully_outputs_info_to_stdout + ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + + assert_equal $stdout.string, "Created database '#{@database}'\n" + end + def test_db_create_when_file_exists File.stubs(:exist?).returns(true) - $stderr.expects(:puts).with("#{@database} already exists") - ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + + assert_equal $stderr.string, "Database '#{@database}' already exists\n" end def test_db_create_with_file_does_nothing @@ -53,7 +67,7 @@ module ActiveRecord $stderr.expects(:puts). with("Couldn't create database for #{@configuration.inspect}") - ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' + assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration, '/rails/root' } end end @@ -69,6 +83,13 @@ module ActiveRecord Pathname.stubs(:new).returns(@path) File.stubs(:join).returns('/former/relative/path') FileUtils.stubs(:rm).returns(true) + + $stdout, @original_stdout = StringIO.new, $stdout + $stderr, @original_stderr = StringIO.new, $stderr + end + + def teardown + $stdout, $stderr = @original_stdout, @original_stderr end def test_creates_path_from_database @@ -103,6 +124,12 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root' end + + def test_when_db_dropped_successfully_outputs_info_to_stdout + ActiveRecord::Tasks::DatabaseTasks.drop @configuration, '/rails/root' + + assert_equal $stdout.string, "Dropped database '#{@database}'\n" + end end class SqliteDBCharsetTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index e0b01ae8e0..87299c0dab 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -65,6 +65,24 @@ module ActiveRecord end end + class PostgreSQLTestCase < TestCase + def self.run(*args) + super if current_adapter?(:PostgreSQLAdapter) + end + end + + class Mysql2TestCase < TestCase + def self.run(*args) + super if current_adapter?(:Mysql2Adapter) + end + end + + class SQLite3TestCase < TestCase + def self.run(*args) + super if current_adapter?(:SQLite3Adapter) + end + end + class SQLCounter class << self attr_accessor :ignored_sql, :log, :log_all @@ -79,9 +97,9 @@ module ActiveRecord # ignored SQL, or better yet, use a different notification for the queries # instead examining the SQL content. oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im] - mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /] + mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im] postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i] - sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im] + sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im] [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql| ignored_sql.concat db_ignored_sql diff --git a/activerecord/test/cases/test_fixtures_test.rb b/activerecord/test/cases/test_fixtures_test.rb index 3f4baf8378..1970fe82d0 100644 --- a/activerecord/test/cases/test_fixtures_test.rb +++ b/activerecord/test/cases/test_fixtures_test.rb @@ -28,7 +28,7 @@ class TestFixturesTest < ActiveRecord::TestCase assert_equal true, @klass.use_transactional_tests end - def test_use_transactional_tests_can_be_overriden + def test_use_transactional_tests_can_be_overridden @klass.use_transactional_tests = "foobar" assert_equal "foobar", @klass.use_transactional_tests diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index ff7a81fe60..628a8eb771 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -1,7 +1,7 @@ require 'cases/helper' require 'support/schema_dumping_helper' -if ActiveRecord::Base.connection.supports_datetime_with_precision? +if subsecond_precision_supported? class TimePrecisionTest < ActiveRecord::TestCase include SchemaDumpingHelper self.use_transactional_tests = false @@ -10,6 +10,7 @@ class TimePrecisionTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection + Foo.reset_column_information end teardown do @@ -20,8 +21,8 @@ class TimePrecisionTest < ActiveRecord::TestCase @connection.create_table(:foos, force: true) @connection.add_column :foos, :start, :time, precision: 3 @connection.add_column :foos, :finish, :time, precision: 6 - assert_equal 3, activerecord_column_option('foos', 'start', 'precision') - assert_equal 6, activerecord_column_option('foos', 'finish', 'precision') + assert_equal 3, Foo.columns_hash['start'].precision + assert_equal 6, Foo.columns_hash['finish'].precision end def test_passing_precision_to_time_does_not_set_limit @@ -29,8 +30,8 @@ class TimePrecisionTest < ActiveRecord::TestCase t.time :start, precision: 3 t.time :finish, precision: 6 end - assert_nil activerecord_column_option('foos', 'start', 'limit') - assert_nil activerecord_column_option('foos', 'finish', 'limit') + assert_nil Foo.columns_hash['start'].limit + assert_nil Foo.columns_hash['finish'].limit end def test_invalid_time_precision_raises_error @@ -42,15 +43,6 @@ class TimePrecisionTest < ActiveRecord::TestCase end end - def test_database_agrees_with_activerecord_about_precision - @connection.create_table(:foos, force: true) do |t| - t.time :start, precision: 2 - t.time :finish, precision: 4 - end - assert_equal 2, database_datetime_precision('foos', 'start') - assert_equal 4, database_datetime_precision('foos', 'finish') - end - def test_formatting_time_according_to_precision @connection.create_table(:foos, force: true) do |t| t.time :start, precision: 0 @@ -88,21 +80,5 @@ class TimePrecisionTest < ActiveRecord::TestCase end end - private - - def database_datetime_precision(table_name, column_name) - results = @connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'") - result = results.find do |result_hash| - result_hash["column_name"] == column_name - end - result && result["datetime_precision"].to_i - end - - def activerecord_column_option(tablename, column_name, option) - result = @connection.columns(tablename).find do |column| - column.name == column_name - end - result && result.send(option) - end end end diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 7c89b4b9e8..937b84bccc 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -84,7 +84,9 @@ class TimestampTest < ActiveRecord::TestCase def test_touching_an_attribute_updates_timestamp previously_created_at = @developer.created_at - @developer.touch(:created_at) + travel(1.second) do + @developer.touch(:created_at) + end assert !@developer.created_at_changed? , 'created_at should not be changed' assert !@developer.changed?, 'record should not be changed' @@ -96,8 +98,11 @@ class TimestampTest < ActiveRecord::TestCase task = Task.first previous_value = task.ending task.touch(:ending) + + now = Time.now.change(usec: 0) + assert_not_equal previous_value, task.ending - assert_in_delta Time.now, task.ending, 1 + assert_in_delta now, task.ending, 1 end def test_touching_an_attribute_updates_timestamp_with_given_time @@ -118,10 +123,12 @@ class TimestampTest < ActiveRecord::TestCase previous_ending = task.ending task.touch(:starting, :ending) + now = Time.now.change(usec: 0) + assert_not_equal previous_starting, task.starting assert_not_equal previous_ending, task.ending - assert_in_delta Time.now, task.starting, 1 - assert_in_delta Time.now, task.ending, 1 + assert_in_delta now, task.starting, 1 + assert_in_delta now, task.ending, 1 end def test_touching_a_record_without_timestamps_is_unexceptional @@ -199,8 +206,10 @@ class TimestampTest < ActiveRecord::TestCase owner = pet.owner previously_owner_updated_at = owner.updated_at - pet.name = "Fluffy the Third" - pet.save + travel(1.second) do + pet.name = "Fluffy the Third" + pet.save + end assert_not_equal previously_owner_updated_at, pet.owner.updated_at end @@ -210,7 +219,9 @@ class TimestampTest < ActiveRecord::TestCase owner = pet.owner previously_owner_updated_at = owner.updated_at - pet.destroy + travel(1.second) do + pet.destroy + end assert_not_equal previously_owner_updated_at, pet.owner.updated_at end @@ -254,8 +265,10 @@ class TimestampTest < ActiveRecord::TestCase owner.update_columns(happy_at: 3.days.ago) previously_owner_updated_at = owner.updated_at - pet.name = "I'm a parrot" - pet.save + travel(1.second) do + pet.name = "I'm a parrot" + pet.save + end assert_not_equal previously_owner_updated_at, pet.owner.updated_at end @@ -446,6 +459,17 @@ class TimestampTest < ActiveRecord::TestCase toy = Toy.first assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model) end + + def test_index_is_created_for_both_timestamps + ActiveRecord::Base.connection.create_table(:foos, force: true) do |t| + t.timestamps(:foos, null: true, index: true) + end + + indexes = ActiveRecord::Base.connection.indexes('foos') + assert_equal ['created_at', 'updated_at'], indexes.flat_map(&:columns).sort + ensure + ActiveRecord::Base.connection.drop_table(:foos) + end end class TimestampsWithoutTransactionTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb new file mode 100644 index 0000000000..b47769eed7 --- /dev/null +++ b/activerecord/test/cases/touch_later_test.rb @@ -0,0 +1,112 @@ +require 'cases/helper' +require 'models/invoice' +require 'models/line_item' +require 'models/topic' +require 'models/node' +require 'models/tree' + +class TouchLaterTest < ActiveRecord::TestCase + fixtures :nodes, :trees + + def test_touch_laster_raise_if_non_persisted + invoice = Invoice.new + Invoice.transaction do + assert_not invoice.persisted? + assert_raises(ActiveRecord::ActiveRecordError) do + invoice.touch_later + end + end + end + + def test_touch_later_dont_set_dirty_attributes + invoice = Invoice.create! + invoice.touch_later + assert_not invoice.changed? + end + + def test_touch_later_update_the_attributes + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time.to_i, topic.updated_at.to_i + assert_equal time.to_i, topic.created_at.to_i + + Topic.transaction do + topic.touch_later(:created_at) + assert_not_equal time.to_i, topic.updated_at.to_i + assert_not_equal time.to_i, topic.created_at.to_i + + assert_equal time.to_i, topic.reload.updated_at.to_i + assert_equal time.to_i, topic.reload.created_at.to_i + end + assert_not_equal time.to_i, topic.reload.updated_at.to_i + assert_not_equal time.to_i, topic.reload.created_at.to_i + end + + def test_touch_touches_immediately + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time.to_i, topic.updated_at.to_i + assert_equal time.to_i, topic.created_at.to_i + + Topic.transaction do + topic.touch_later(:created_at) + topic.touch + + assert_not_equal time, topic.reload.updated_at + assert_not_equal time, topic.reload.created_at + end + end + + def test_touch_later_an_association_dont_autosave_parent + time = Time.now.utc - 25.days + line_item = LineItem.create!(amount: 1) + invoice = Invoice.create!(line_items: [line_item]) + invoice.touch(time: time) + + Invoice.transaction do + line_item.update(amount: 2) + assert_equal time.to_i, invoice.reload.updated_at.to_i + end + + assert_not_equal time.to_i, invoice.updated_at.to_i + end + + def test_touch_touches_immediately_with_a_custom_time + time = (Time.now.utc - 25.days).change(nsec: 0) + topic = Topic.create!(updated_at: time, created_at: time) + assert_equal time, topic.updated_at + assert_equal time, topic.created_at + + Topic.transaction do + topic.touch_later(:created_at) + time = Time.now.utc - 2.days + topic.touch(time: time) + + assert_equal time.to_i, topic.reload.updated_at.to_i + assert_equal time.to_i, topic.reload.created_at.to_i + end + end + + def test_touch_later_dont_hit_the_db + invoice = Invoice.create! + assert_queries(0) do + invoice.touch_later + end + end + + def test_touching_three_deep + previous_tree_updated_at = trees(:root).updated_at + previous_grandparent_updated_at = nodes(:grandparent).updated_at + previous_parent_updated_at = nodes(:parent_a).updated_at + previous_child_updated_at = nodes(:child_one_of_a).updated_at + + travel 5.seconds do + Node.create! parent: nodes(:child_one_of_a), tree: trees(:root) + end + + assert_not_equal nodes(:child_one_of_a).reload.updated_at, previous_child_updated_at + assert_not_equal nodes(:parent_a).reload.updated_at, previous_parent_updated_at + assert_not_equal nodes(:grandparent).reload.updated_at, previous_grandparent_updated_at + assert_not_equal trees(:root).reload.updated_at, previous_tree_updated_at + end +end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index f2229939c8..8a7f19293d 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -34,10 +34,11 @@ class TransactionCallbacksTest < ActiveRecord::TestCase has_many :replies, class_name: "ReplyWithCallbacks", foreign_key: "parent_id" + before_commit { |record| record.do_before_commit(nil) } after_commit { |record| record.do_after_commit(nil) } - after_commit(on: :create) { |record| record.do_after_commit(:create) } - after_commit(on: :update) { |record| record.do_after_commit(:update) } - after_commit(on: :destroy) { |record| record.do_after_commit(:destroy) } + after_create_commit { |record| record.do_after_commit(:create) } + after_update_commit { |record| record.do_after_commit(:update) } + after_destroy_commit { |record| record.do_after_commit(:destroy) } after_rollback { |record| record.do_after_rollback(nil) } after_rollback(on: :create) { |record| record.do_after_rollback(:create) } after_rollback(on: :update) { |record| record.do_after_rollback(:update) } @@ -47,6 +48,12 @@ class TransactionCallbacksTest < ActiveRecord::TestCase @history ||= [] end + def before_commit_block(on = nil, &block) + @before_commit ||= {} + @before_commit[on] ||= [] + @before_commit[on] << block + end + def after_commit_block(on = nil, &block) @after_commit ||= {} @after_commit[on] ||= [] @@ -59,6 +66,11 @@ class TransactionCallbacksTest < ActiveRecord::TestCase @after_rollback[on] << block end + def do_before_commit(on) + blocks = @before_commit[on] if defined?(@before_commit) + blocks.each{|b| b.call(self)} if blocks + end + def do_after_commit(on) blocks = @after_commit[on] if defined?(@after_commit) blocks.each{|b| b.call(self)} if blocks @@ -74,6 +86,20 @@ class TransactionCallbacksTest < ActiveRecord::TestCase @first = TopicWithCallbacks.find(1) end + # FIXME: Test behavior, not implementation. + def test_before_commit_exception_should_pop_transaction_stack + @first.before_commit_block { raise 'better pop this txn from the stack!' } + + original_txn = @first.class.connection.current_transaction + + begin + @first.save! + fail + rescue + assert_equal original_txn, @first.class.connection.current_transaction + end + end + def test_call_after_commit_after_transaction_commits @first.after_commit_block{|r| r.history << :after_commit} @first.after_rollback_block{|r| r.history << :after_rollback} diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 2468a91969..791b895d02 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -58,6 +58,11 @@ class TransactionTest < ActiveRecord::TestCase end end + def test_add_to_null_transaction + topic = Topic.new + topic.add_to_transaction + end + def test_successful_with_return committed = false @@ -175,13 +180,20 @@ class TransactionTest < ActiveRecord::TestCase assert topic.new_record?, "#{topic.inspect} should be new record" end + def test_transaction_state_is_cleared_when_record_is_persisted + author = Author.create! name: 'foo' + author.name = nil + assert_not author.save + assert_not author.new_record? + end + def test_update_should_rollback_on_failure author = Author.find(1) posts_count = author.posts.size assert posts_count > 0 status = author.update(name: nil, post_ids: []) assert !status - assert_equal posts_count, author.posts(true).size + assert_equal posts_count, author.posts.reload.size end def test_update_should_rollback_on_failure! @@ -191,7 +203,7 @@ class TransactionTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordInvalid) do author.update!(name: nil, post_ids: []) end - assert_equal posts_count, author.posts(true).size + assert_equal posts_count, author.posts.reload.size end def test_cancellation_from_returning_false_in_before_filter @@ -480,13 +492,17 @@ class TransactionTest < ActiveRecord::TestCase end def test_rollback_when_commit_raises - Topic.connection.expects(:begin_db_transaction) - Topic.connection.expects(:commit_db_transaction).raises('OH NOES') - Topic.connection.expects(:rollback_db_transaction) + assert_called(Topic.connection, :begin_db_transaction) do + Topic.connection.stub(:commit_db_transaction, ->{ raise('OH NOES') }) do + assert_called(Topic.connection, :rollback_db_transaction) do - assert_raise RuntimeError do - Topic.transaction do - # do nothing + e = assert_raise RuntimeError do + Topic.transaction do + # do nothing + end + end + assert_equal 'OH NOES', e.message + end end end end diff --git a/activerecord/test/cases/type/date_time_test.rb b/activerecord/test/cases/type/date_time_test.rb new file mode 100644 index 0000000000..bc4900e1c2 --- /dev/null +++ b/activerecord/test/cases/type/date_time_test.rb @@ -0,0 +1,14 @@ +require "cases/helper" +require "models/task" + +module ActiveRecord + module Type + class IntegerTest < ActiveRecord::TestCase + def test_datetime_seconds_precision_applied_to_timestamp + skip "This test is invalid if subsecond precision isn't supported" unless subsecond_precision_supported? + p = Task.create!(starting: ::Time.now) + assert_equal p.starting.usec, p.reload.starting.usec + end + end + end +end diff --git a/activerecord/test/cases/type/decimal_test.rb b/activerecord/test/cases/type/decimal_test.rb deleted file mode 100644 index fe49d0e79a..0000000000 --- a/activerecord/test/cases/type/decimal_test.rb +++ /dev/null @@ -1,51 +0,0 @@ -require "cases/helper" - -module ActiveRecord - module Type - class DecimalTest < ActiveRecord::TestCase - def test_type_cast_decimal - type = Decimal.new - assert_equal BigDecimal.new("0"), type.cast(BigDecimal.new("0")) - assert_equal BigDecimal.new("123"), type.cast(123.0) - assert_equal BigDecimal.new("1"), type.cast(:"1") - end - - def test_type_cast_decimal_from_float_with_large_precision - type = Decimal.new(precision: ::Float::DIG + 2) - assert_equal BigDecimal.new("123.0"), type.cast(123.0) - end - - def test_type_cast_from_float_with_unspecified_precision - type = Decimal.new - assert_equal 22.68.to_d, type.cast(22.68) - end - - def test_type_cast_decimal_from_rational_with_precision - type = Decimal.new(precision: 2) - assert_equal BigDecimal("0.33"), type.cast(Rational(1, 3)) - end - - def test_type_cast_decimal_from_rational_without_precision_defaults_to_18_36 - type = Decimal.new - assert_equal BigDecimal("0.333333333333333333E0"), type.cast(Rational(1, 3)) - end - - def test_type_cast_decimal_from_object_responding_to_d - value = Object.new - def value.to_d - BigDecimal.new("1") - end - type = Decimal.new - assert_equal BigDecimal("1"), type.cast(value) - end - - def test_changed? - type = Decimal.new - - assert type.changed?(5.0, 5.0, '5.0wibble') - assert_not type.changed?(5.0, 5.0, '5.0') - assert_not type.changed?(-5.0, -5.0, '-5.0') - end - end - end -end diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb index 84fb05dd8e..c0932d5357 100644 --- a/activerecord/test/cases/type/integer_test.rb +++ b/activerecord/test/cases/type/integer_test.rb @@ -4,112 +4,12 @@ require "models/company" module ActiveRecord module Type class IntegerTest < ActiveRecord::TestCase - test "simple values" do - type = Type::Integer.new - assert_equal 1, type.cast(1) - assert_equal 1, type.cast('1') - assert_equal 1, type.cast('1ignore') - assert_equal 0, type.cast('bad1') - assert_equal 0, type.cast('bad') - assert_equal 1, type.cast(1.7) - assert_equal 0, type.cast(false) - assert_equal 1, type.cast(true) - assert_nil type.cast(nil) - end - - test "random objects cast to nil" do - type = Type::Integer.new - assert_nil type.cast([1,2]) - assert_nil type.cast({1 => 2}) - assert_nil type.cast((1..2)) - end - test "casting ActiveRecord models" do type = Type::Integer.new firm = Firm.create(:name => 'Apple') assert_nil type.cast(firm) end - test "casting objects without to_i" do - type = Type::Integer.new - assert_nil type.cast(::Object.new) - end - - test "casting nan and infinity" do - type = Type::Integer.new - assert_nil type.cast(::Float::NAN) - assert_nil type.cast(1.0/0.0) - end - - test "casting booleans for database" do - type = Type::Integer.new - assert_equal 1, type.serialize(true) - assert_equal 0, type.serialize(false) - end - - test "changed?" do - type = Type::Integer.new - - assert type.changed?(5, 5, '5wibble') - assert_not type.changed?(5, 5, '5') - assert_not type.changed?(5, 5, '5.0') - assert_not type.changed?(-5, -5, '-5') - assert_not type.changed?(-5, -5, '-5.0') - assert_not type.changed?(nil, nil, nil) - end - - test "values below int min value are out of range" do - assert_raises(::RangeError) do - Integer.new.serialize(-2147483649) - end - end - - test "values above int max value are out of range" do - assert_raises(::RangeError) do - Integer.new.serialize(2147483648) - end - end - - test "very small numbers are out of range" do - assert_raises(::RangeError) do - Integer.new.serialize(-9999999999999999999999999999999) - end - end - - test "very large numbers are out of range" do - assert_raises(::RangeError) do - Integer.new.serialize(9999999999999999999999999999999) - end - end - - test "normal numbers are in range" do - type = Integer.new - assert_equal(0, type.serialize(0)) - assert_equal(-1, type.serialize(-1)) - assert_equal(1, type.serialize(1)) - end - - test "int max value is in range" do - assert_equal(2147483647, Integer.new.serialize(2147483647)) - end - - test "int min value is in range" do - assert_equal(-2147483648, Integer.new.serialize(-2147483648)) - end - - test "columns with a larger limit have larger ranges" do - type = Integer.new(limit: 8) - - assert_equal(9223372036854775807, type.serialize(9223372036854775807)) - assert_equal(-9223372036854775808, type.serialize(-9223372036854775808)) - assert_raises(::RangeError) do - type.serialize(-9999999999999999999999999999999) - end - assert_raises(::RangeError) do - type.serialize(9999999999999999999999999999999) - end - end - test "values which are out of range can be re-assigned" do klass = Class.new(ActiveRecord::Base) do self.table_name = 'posts' diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb index 56e9bf434d..6fe6d46711 100644 --- a/activerecord/test/cases/type/string_test.rb +++ b/activerecord/test/cases/type/string_test.rb @@ -2,20 +2,6 @@ require 'cases/helper' module ActiveRecord class StringTypeTest < ActiveRecord::TestCase - test "type casting" do - type = Type::String.new - assert_equal "t", type.cast(true) - assert_equal "f", type.cast(false) - assert_equal "123", type.cast(123) - end - - test "values are duped coming out" do - s = "foo" - type = Type::String.new - assert_not_same s, type.cast(s) - assert_not_same s, type.deserialize(s) - end - test "string mutations are detected" do klass = Class.new(Base) klass.table_name = 'authors' diff --git a/activerecord/test/cases/type/unsigned_integer_test.rb b/activerecord/test/cases/type/unsigned_integer_test.rb deleted file mode 100644 index f2c910eade..0000000000 --- a/activerecord/test/cases/type/unsigned_integer_test.rb +++ /dev/null @@ -1,17 +0,0 @@ -require "cases/helper" - -module ActiveRecord - module Type - class UnsignedIntegerTest < ActiveRecord::TestCase - test "unsigned int max value is in range" do - assert_equal(4294967295, UnsignedInteger.new.serialize(4294967295)) - end - - test "minus value is out of range" do - assert_raises(::RangeError) do - UnsignedInteger.new.serialize(-1) - end - end - end - end -end diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb index 9b1859c2ce..81fcf04a27 100644 --- a/activerecord/test/cases/types_test.rb +++ b/activerecord/test/cases/types_test.rb @@ -3,111 +3,6 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters class TypesTest < ActiveRecord::TestCase - def test_type_cast_boolean - type = Type::Boolean.new - assert type.cast('').nil? - assert type.cast(nil).nil? - - assert type.cast(true) - assert type.cast(1) - assert type.cast('1') - assert type.cast('t') - assert type.cast('T') - assert type.cast('true') - assert type.cast('TRUE') - assert type.cast('on') - assert type.cast('ON') - assert type.cast(' ') - assert type.cast("\u3000\r\n") - assert type.cast("\u0000") - assert type.cast('SOMETHING RANDOM') - - # explicitly check for false vs nil - assert_equal false, type.cast(false) - assert_equal false, type.cast(0) - assert_equal false, type.cast('0') - assert_equal false, type.cast('f') - assert_equal false, type.cast('F') - assert_equal false, type.cast('false') - assert_equal false, type.cast('FALSE') - assert_equal false, type.cast('off') - assert_equal false, type.cast('OFF') - end - - def test_type_cast_float - type = Type::Float.new - assert_equal 1.0, type.cast("1") - end - - def test_changing_float - type = Type::Float.new - - assert type.changed?(5.0, 5.0, '5wibble') - assert_not type.changed?(5.0, 5.0, '5') - assert_not type.changed?(5.0, 5.0, '5.0') - assert_not type.changed?(nil, nil, nil) - end - - def test_type_cast_binary - type = Type::Binary.new - assert_equal nil, type.cast(nil) - assert_equal "1", type.cast("1") - assert_equal 1, type.cast(1) - end - - def test_type_cast_time - type = Type::Time.new - assert_equal nil, type.cast(nil) - assert_equal nil, type.cast('') - assert_equal nil, type.cast('ABC') - - time_string = Time.now.utc.strftime("%T") - assert_equal time_string, type.cast(time_string).strftime("%T") - end - - def test_type_cast_datetime_and_timestamp - type = Type::DateTime.new - assert_equal nil, type.cast(nil) - assert_equal nil, type.cast('') - assert_equal nil, type.cast(' ') - assert_equal nil, type.cast('ABC') - - datetime_string = Time.now.utc.strftime("%FT%T") - assert_equal datetime_string, type.cast(datetime_string).strftime("%FT%T") - end - - def test_type_cast_date - type = Type::Date.new - assert_equal nil, type.cast(nil) - assert_equal nil, type.cast('') - assert_equal nil, type.cast(' ') - assert_equal nil, type.cast('ABC') - - date_string = Time.now.utc.strftime("%F") - assert_equal date_string, type.cast(date_string).strftime("%F") - end - - def test_type_cast_duration_to_integer - type = Type::Integer.new - assert_equal 1800, type.cast(30.minutes) - assert_equal 7200, type.cast(2.hours) - end - - def test_string_to_time_with_timezone - [:utc, :local].each do |zone| - with_timezone_config default: zone do - type = Type::DateTime.new - assert_equal Time.utc(2013, 9, 4, 0, 0, 0), type.cast("Wed, 04 Sep 2013 03:00:00 EAT") - end - end - end - - def test_type_equality - assert_equal Type::Value.new, Type::Value.new - assert_not_equal Type::Value.new, Type::Integer.new - assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2) - end - def test_attributes_which_are_invalid_for_database_can_still_be_reassigned type_which_cannot_go_to_the_database = Type::Value.new def type_which_cannot_go_to_the_database.serialize(*) diff --git a/activerecord/test/cases/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb new file mode 100644 index 0000000000..c0b3750bcc --- /dev/null +++ b/activerecord/test/cases/validations/absence_validation_test.rb @@ -0,0 +1,73 @@ +require "cases/helper" +require 'models/face' +require 'models/interest' +require 'models/man' +require 'models/topic' + +class AbsenceValidationTest < ActiveRecord::TestCase + def test_non_association + boy_klass = Class.new(Man) do + def self.name; "Boy" end + validates_absence_of :name + end + + assert boy_klass.new.valid? + assert_not boy_klass.new(name: "Alex").valid? + end + + def test_has_one_marked_for_destruction + boy_klass = Class.new(Man) do + def self.name; "Boy" end + validates_absence_of :face + end + + boy = boy_klass.new(face: Face.new) + assert_not boy.valid?, "should not be valid if has_one association is present" + assert_equal 1, boy.errors[:face].size, "should only add one error" + + boy.face.mark_for_destruction + assert boy.valid?, "should be valid if association is marked for destruction" + end + + def test_has_many_marked_for_destruction + boy_klass = Class.new(Man) do + def self.name; "Boy" end + validates_absence_of :interests + end + boy = boy_klass.new + boy.interests << [i1 = Interest.new, i2 = Interest.new] + assert_not boy.valid?, "should not be valid if has_many association is present" + + i1.mark_for_destruction + assert_not boy.valid?, "should not be valid if has_many association is present" + + i2.mark_for_destruction + assert boy.valid? + end + + def test_does_not_call_to_a_on_associations + boy_klass = Class.new(Man) do + def self.name; "Boy" end + validates_absence_of :face + end + + face_with_to_a = Face.new + def face_with_to_a.to_a; ['(/)', '(\)']; end + + assert_nothing_raised { boy_klass.new(face: face_with_to_a).valid? } + end + + def test_validates_absence_of_virtual_attribute_on_model + repair_validations(Interest) do + Interest.send(:attr_accessor, :token) + Interest.validates_absence_of(:token) + + interest = Interest.create!(topic: 'Thought Leadering') + assert interest.valid? + + interest.token = 'tl' + + assert interest.invalid? + end + end +end diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index bff5ffa65e..584a3dc0d8 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -45,6 +45,18 @@ class AssociationValidationTest < ActiveRecord::TestCase assert t.valid? end + def test_validates_associated_without_marked_for_destruction + reply = Class.new do + def valid? + true + end + end + Topic.validates_associated(:replies) + t = Topic.new + t.define_singleton_method(:replies) { [reply.new] } + assert t.valid? + end + def test_validates_associated_with_custom_message_using_quotes Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes" Topic.validates_presence_of :content diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index 268d7914b5..b8307d6665 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -47,29 +47,25 @@ class I18nValidationTest < ActiveRecord::TestCase # [ "given on condition", {on: :save}, {}] ] - # validates_uniqueness_of w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_uniqueness_of on generated message #{name}" do Topic.validates_uniqueness_of :title, validation_options @topic.title = unique_topic.title - @topic.errors.expects(:generate_message).with(:title, :taken, generate_message_options.merge(:value => 'unique!')) - @topic.valid? + assert_called_with(@topic.errors, :generate_message, [:title, :taken, generate_message_options.merge(:value => 'unique!')]) do + @topic.valid? + end end end - # validates_associated w/ mocha - COMMON_CASES.each do |name, validation_options, generate_message_options| test "validates_associated on generated message #{name}" do Topic.validates_associated :replies, validation_options - replied_topic.errors.expects(:generate_message).with(:replies, :invalid, generate_message_options.merge(:value => replied_topic.replies)) - replied_topic.save + assert_called_with(replied_topic.errors, :generate_message, [:replies, :invalid, generate_message_options.merge(:value => replied_topic.replies)]) do + replied_topic.save + end end end - # validates_associated w/o mocha - def test_validates_associated_finds_custom_model_key_translation I18n.backend.store_translations 'en', :activerecord => {:errors => {:models => {:topic => {:attributes => {:replies => {:invalid => 'custom message'}}}}}} I18n.backend.store_translations 'en', :activerecord => {:errors => {:messages => {:invalid => 'global message'}}} diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index f95f8f0b8f..78263fd955 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- require "cases/helper" require 'models/owner' require 'models/pet' @@ -62,17 +61,19 @@ class LengthValidationTest < ActiveRecord::TestCase assert_equal pet_count, Pet.count end - def test_does_not_validate_length_of_if_parent_record_is_validate_false - @owner.validates_length_of :name, minimum: 1 - owner = @owner.new - owner.save!(validate: false) - assert owner.persisted? + def test_validates_length_of_virtual_attribute_on_model + repair_validations(Pet) do + Pet.send(:attr_accessor, :nickname) + Pet.validates_length_of(:name, minimum: 1) + Pet.validates_length_of(:nickname, minimum: 1) - pet = Pet.new(owner_id: owner.id) - pet.save! + pet = Pet.create!(name: 'Fancy Pants', nickname: 'Fancy') - assert_equal owner.pets.size, 1 - assert owner.valid? - assert pet.valid? + assert pet.valid? + + pet.nickname = '' + + assert pet.invalid? + end end end diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb index 6f8ad06ab6..868d111b8c 100644 --- a/activerecord/test/cases/validations/presence_validation_test.rb +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -65,19 +65,39 @@ class PresenceValidationTest < ActiveRecord::TestCase assert_nothing_raised { s.valid? } end - def test_does_not_validate_presence_of_if_parent_record_is_validate_false + def test_validates_presence_of_virtual_attribute_on_model repair_validations(Interest) do + Interest.send(:attr_accessor, :abbreviation) Interest.validates_presence_of(:topic) + Interest.validates_presence_of(:abbreviation) + + interest = Interest.create!(topic: 'Thought Leadering', abbreviation: 'tl') + assert interest.valid? + + interest.abbreviation = '' + + assert interest.invalid? + end + end + + def test_validations_run_on_persisted_record + repair_validations(Interest) do interest = Interest.new - interest.save!(validate: false) - assert interest.persisted? + interest.save! + assert_predicate interest, :valid? - man = Man.new(interest_ids: [interest.id]) - man.save! + Interest.validates_presence_of(:topic) - assert_equal man.interests.size, 1 - assert interest.valid? - assert man.valid? + assert_not_predicate interest, :valid? + end + end + + def test_validates_presence_with_on_context + repair_validations(Interest) do + Interest.validates_presence_of(:topic, on: :required_name) + interest = Interest.new + interest.save! + assert_not interest.valid?(:required_name) end end end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 062bc733f9..4b0a590adb 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -4,6 +4,8 @@ require 'models/reply' require 'models/warehouse_thing' require 'models/guid' require 'models/event' +require 'models/dashboard' +require 'models/uuid_item' class Wizard < ActiveRecord::Base self.abstract_class = true @@ -34,7 +36,34 @@ class TopicWithUniqEvent < Topic validates :event, uniqueness: true end +class BigIntTest < ActiveRecord::Base + INT_MAX_VALUE = 2147483647 + self.table_name = 'cars' + validates :engines_count, uniqueness: true, inclusion: { in: 0..INT_MAX_VALUE } +end + +class BigIntReverseTest < ActiveRecord::Base + INT_MAX_VALUE = 2147483647 + self.table_name = 'cars' + validates :engines_count, inclusion: { in: 0..INT_MAX_VALUE } + validates :engines_count, uniqueness: true +end + +class CoolTopic < Topic + validates_uniqueness_of :id +end + +class TopicWithAfterCreate < Topic + after_create :set_author + + def set_author + update_attributes!(:author_name => "#{title} #{id}") + end +end + class UniquenessValidationTest < ActiveRecord::TestCase + INT_MAX_VALUE = 2147483647 + fixtures :topics, 'warehouse-things' repair_validations(Topic, Reply) @@ -86,6 +115,16 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert t2.errors[:title] end + def test_validate_uniqueness_when_integer_out_of_range + entry = BigIntTest.create(engines_count: INT_MAX_VALUE + 1) + assert_equal entry.errors[:engines_count], ['is not included in the list'] + end + + def test_validate_uniqueness_when_integer_out_of_range_show_order_does_not_matter + entry = BigIntReverseTest.create(engines_count: INT_MAX_VALUE + 1) + assert_equal entry.errors[:engines_count], ['is not included in the list'] + end + def test_validates_uniqueness_with_newline_chars Topic.validates_uniqueness_of(:title, :case_sensitive => false) @@ -310,19 +349,41 @@ class UniquenessValidationTest < ActiveRecord::TestCase end def test_validate_uniqueness_with_limit - # Event.title is limited to 5 characters - e1 = Event.create(:title => "abcde") - assert e1.valid?, "Could not create an event with a unique, 5 character title" - e2 = Event.create(:title => "abcdefgh") - assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique" + if current_adapter?(:SQLite3Adapter) + # Event.title has limit 5, but SQLite doesn't truncate. + e1 = Event.create(title: "abcdefgh") + assert e1.valid?, "Could not create an event with a unique 8 characters title" + + e2 = Event.create(title: "abcdefgh") + assert_not e2.valid?, "Created an event whose title is not unique" + elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter) + assert_raise(ActiveRecord::ValueTooLong) do + Event.create(title: "abcdefgh") + end + else + assert_raise(ActiveRecord::StatementInvalid) do + Event.create(title: "abcdefgh") + end + end end def test_validate_uniqueness_with_limit_and_utf8 - # Event.title is limited to 5 characters - e1 = Event.create(:title => "一二三四五") - assert e1.valid?, "Could not create an event with a unique, 5 character title" - e2 = Event.create(:title => "一二三四五六七八") - assert !e2.valid?, "Created an event whose title, with limit taken into account, is not unique" + if current_adapter?(:SQLite3Adapter) + # Event.title has limit 5, but does SQLite doesn't truncate. + e1 = Event.create(title: "一二三四五六七八") + assert e1.valid?, "Could not create an event with a unique 8 characters title" + + e2 = Event.create(title: "一二三四五六七八") + assert_not e2.valid?, "Created an event whose title is not unique" + elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter) + assert_raise(ActiveRecord::ValueTooLong) do + Event.create(title: "一二三四五六七八") + end + else + assert_raise(ActiveRecord::StatementInvalid) do + Event.create(title: "一二三四五六七八") + end + end end def test_validate_straight_inheritance_uniqueness @@ -386,20 +447,86 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert topic.valid? end - def test_does_not_validate_uniqueness_of_if_parent_record_is_validate_false - Reply.validates_uniqueness_of(:content) + def test_validate_uniqueness_of_custom_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = "keyboards" + self.primary_key = :key_number - Reply.create!(content: "Topic Title") + validates_uniqueness_of :key_number - reply = Reply.new(content: "Topic Title") - reply.save!(validate: false) - assert reply.persisted? + def self.name + "Keyboard" + end + end - topic = Topic.new(reply_ids: [reply.id]) - topic.save! + klass.create!(key_number: 10) + key2 = klass.create!(key_number: 11) - assert_equal topic.replies.size, 1 - assert reply.valid? - assert topic.valid? + key2.key_number = 10 + assert_not key2.valid? + end + + def test_validate_uniqueness_without_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = "dashboards" + + validates_uniqueness_of :dashboard_id + + def self.name; "Dashboard" end + end + + abc = klass.create!(dashboard_id: "abc") + assert klass.new(dashboard_id: "xyz").valid? + assert_not klass.new(dashboard_id: "abc").valid? + + abc.dashboard_id = "def" + + e = assert_raises ActiveRecord::UnknownPrimaryKey do + abc.save! + end + assert_match(/\AUnknown primary key for table dashboards in model/, e.message) + assert_match(/Can not validate uniqueness for persisted record without primary key.\z/, e.message) + end + + def test_validate_uniqueness_ignores_itself_when_primary_key_changed + Topic.validates_uniqueness_of(:title) + + t = Topic.new("title" => "This is a unique title") + assert t.save, "Should save t as unique" + + t.id += 1 + assert t.valid?, "Should be valid" + assert t.save, "Should still save t as unique" + end + + def test_validate_uniqueness_with_after_create_performing_save + TopicWithAfterCreate.validates_uniqueness_of(:title) + topic = TopicWithAfterCreate.create!(:title => "Title1") + assert topic.author_name.start_with?("Title1") + + topic2 = TopicWithAfterCreate.new(:title => "Title1") + refute topic2.valid? + assert_equal(["has already been taken"], topic2.errors[:title]) + end + + def test_validate_uniqueness_uuid + skip unless current_adapter?(:PostgreSQLAdapter) + item = UuidItem.create!(uuid: SecureRandom.uuid, title: 'item1') + item.update(title: 'item1-title2') + assert_empty item.errors + + item2 = UuidValidatingItem.create!(uuid: SecureRandom.uuid, title: 'item2') + item2.update(title: 'item2-title2') + assert_empty item2.errors + end + + def test_validate_uniqueness_regular_id + item = CoolTopic.create!(title: 'MyItem') + assert_empty item.errors + + item2 = CoolTopic.new(id: item.id, title: 'MyItem2') + refute item2.valid? + + assert_equal(["has already been taken"], item2.errors[:id]) end end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index f4f316f393..85e33d2218 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -52,6 +52,13 @@ class ValidationsTest < ActiveRecord::TestCase assert r.valid?(:special_case) end + def test_invalid_using_multiple_contexts + r = WrongReply.new(:title => 'Wrong Create') + assert r.invalid?([:special_case, :create]) + assert_equal "Invalid", r.errors[:author_name].join + assert_equal "is Wrong Create", r.errors[:title].join + end + def test_validate r = WrongReply.new @@ -123,7 +130,7 @@ class ValidationsTest < ActiveRecord::TestCase def test_validates_acceptance_of_with_non_existent_table Object.const_set :IncorporealModel, Class.new(ActiveRecord::Base) - assert_nothing_raised ActiveRecord::StatementInvalid do + assert_nothing_raised do IncorporealModel.validates_acceptance_of(:incorporeal_column) end end @@ -161,4 +168,15 @@ class ValidationsTest < ActiveRecord::TestCase ensure Topic.reset_column_information end + + def test_acceptance_validator_doesnt_require_db_connection + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'posts' + end + klass.reset_column_information + + assert_no_queries do + klass.validates_acceptance_of(:foo) + end + end end diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index 3aed90ba36..f3c2d2f30e 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -1,7 +1,9 @@ require "cases/helper" require "models/book" +require "support/schema_dumping_helper" module ViewBehavior + include SchemaDumpingHelper extend ActiveSupport::Concern included do @@ -31,9 +33,24 @@ module ViewBehavior assert_equal ["Ruby for Rails"], books.map(&:name) end + def test_views + assert_equal [Ebook.table_name], @connection.views + end + + def test_view_exists + view_name = Ebook.table_name + assert @connection.view_exists?(view_name), "'#{view_name}' view should exist" + end + def test_table_exists view_name = Ebook.table_name - assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" + # TODO: switch this assertion around once we changed #tables to not return views. + ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" } + end + + def test_views_ara_valid_data_sources + view_name = Ebook.table_name + assert @connection.data_source_exists?(view_name), "'#{view_name}' should be a data source" end def test_column_definitions @@ -53,6 +70,11 @@ module ViewBehavior end assert_nil model.primary_key end + + def test_does_not_dump_view_as_table + schema = dump_table_schema "ebooks" + assert_no_match %r{create_table "ebooks"}, schema + end end if ActiveRecord::Base.connection.supports_views? @@ -65,11 +87,12 @@ class ViewWithPrimaryKeyTest < ActiveRecord::TestCase end def drop_view(name) - @connection.execute "DROP VIEW #{name}" if @connection.table_exists? name + @connection.execute "DROP VIEW #{name}" if @connection.view_exists? name end end class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase + include SchemaDumpingHelper fixtures :books class Paperback < ActiveRecord::Base; end @@ -83,7 +106,7 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase end teardown do - @connection.execute "DROP VIEW paperbacks" if @connection.table_exists? "paperbacks" + @connection.execute "DROP VIEW paperbacks" if @connection.view_exists? "paperbacks" end def test_reading @@ -91,9 +114,19 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase assert_equal ["Agile Web Development with Rails"], books.map(&:name) end + def test_views + assert_equal [Paperback.table_name], @connection.views + end + + def test_view_exists + view_name = Paperback.table_name + assert @connection.view_exists?(view_name), "'#{view_name}' view should exist" + end + def test_table_exists view_name = Paperback.table_name - assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" + # TODO: switch this assertion around once we changed #tables to not return views. + ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" } end def test_column_definitions @@ -102,12 +135,82 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase end def test_attributes - assert_equal({"name" => "Agile Web Development with Rails", "status" => 0}, + assert_equal({"name" => "Agile Web Development with Rails", "status" => 2}, Paperback.first.attributes) end def test_does_not_have_a_primary_key assert_nil Paperback.primary_key end + + def test_does_not_dump_view_as_table + schema = dump_table_schema "paperbacks" + assert_no_match %r{create_table "paperbacks"}, schema + end +end + +# sqlite dose not support CREATE, INSERT, and DELETE for VIEW +if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) +class UpdateableViewTest < ActiveRecord::TestCase + self.use_transactional_tests = false + fixtures :books + + class PrintedBook < ActiveRecord::Base + self.primary_key = "id" + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.execute <<-SQL + CREATE VIEW printed_books + AS SELECT id, name, status, format FROM books WHERE format = 'paperback' + SQL + end + + teardown do + @connection.execute "DROP VIEW printed_books" if @connection.view_exists? "printed_books" + end + + def test_update_record + book = PrintedBook.first + book.name = "AWDwR" + book.save! + book.reload + assert_equal "AWDwR", book.name + end + + def test_insert_record + PrintedBook.create! name: "Rails in Action", status: 0, format: "paperback" + + new_book = PrintedBook.last + assert_equal "Rails in Action", new_book.name + end + + def test_update_record_to_fail_view_conditions + book = PrintedBook.first + book.format = "ebook" + book.save! + + assert_raises ActiveRecord::RecordNotFound do + book.reload + end + end +end +end # end fo `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)` +end # end fo `if ActiveRecord::Base.connection.supports_views?` + +if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) && + ActiveRecord::Base.connection.supports_materialized_views? +class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase + include ViewBehavior + + private + def create_view(name, query) + @connection.execute "CREATE MATERIALIZED VIEW #{name} AS #{query}" + end + + def drop_view(name) + @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.view_exists? name + end end end diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb deleted file mode 100644 index b30b50f597..0000000000 --- a/activerecord/test/cases/xml_serialization_test.rb +++ /dev/null @@ -1,447 +0,0 @@ -require "cases/helper" -require "rexml/document" -require 'models/contact' -require 'models/post' -require 'models/author' -require 'models/comment' -require 'models/company_in_module' -require 'models/toy' -require 'models/topic' -require 'models/reply' -require 'models/company' - -class XmlSerializationTest < ActiveRecord::TestCase - def test_should_serialize_default_root - @xml = Contact.new.to_xml - assert_match %r{^<contact>}, @xml - assert_match %r{</contact>$}, @xml - end - - def test_should_serialize_default_root_with_namespace - @xml = Contact.new.to_xml :namespace=>"http://xml.rubyonrails.org/contact" - assert_match %r{^<contact xmlns="http://xml\.rubyonrails\.org/contact">}, @xml - assert_match %r{</contact>$}, @xml - end - - def test_should_serialize_custom_root - @xml = Contact.new.to_xml :root => 'xml_contact' - assert_match %r{^<xml-contact>}, @xml - assert_match %r{</xml-contact>$}, @xml - end - - def test_should_allow_undasherized_tags - @xml = Contact.new.to_xml :root => 'xml_contact', :dasherize => false - assert_match %r{^<xml_contact>}, @xml - assert_match %r{</xml_contact>$}, @xml - assert_match %r{<created_at}, @xml - end - - def test_should_allow_camelized_tags - @xml = Contact.new.to_xml :root => 'xml_contact', :camelize => true - assert_match %r{^<XmlContact>}, @xml - assert_match %r{</XmlContact>$}, @xml - assert_match %r{<CreatedAt}, @xml - end - - def test_should_allow_skipped_types - @xml = Contact.new(:age => 25).to_xml :skip_types => true - assert %r{<age>25</age>}.match(@xml) - end - - def test_should_include_yielded_additions - @xml = Contact.new.to_xml do |xml| - xml.creator "David" - end - assert_match %r{<creator>David</creator>}, @xml - end - - def test_to_xml_with_block - value = "Rockin' the block" - xml = Contact.new.to_xml(:skip_instruct => true) do |_xml| - _xml.tag! "arbitrary-element", value - end - assert_equal "<contact>", xml.first(9) - assert xml.include?(%(<arbitrary-element>#{value}</arbitrary-element>)) - end - - def test_should_skip_instruct_for_included_records - @contact = Contact.new - @contact.alternative = Contact.new(:name => 'Copa Cabana') - @xml = @contact.to_xml(:include => [ :alternative ]) - assert_equal @xml.index('<?xml '), 0 - assert_nil @xml.index('<?xml ', 1) - end -end - -class DefaultXmlSerializationTest < ActiveRecord::TestCase - def setup - @contact = Contact.new( - :name => 'aaron stack', - :age => 25, - :avatar => 'binarydata', - :created_at => Time.utc(2006, 8, 1), - :awesome => false, - :preferences => { :gem => 'ruby' } - ) - end - - def test_should_serialize_string - assert_match %r{<name>aaron stack</name>}, @contact.to_xml - end - - def test_should_serialize_integer - assert_match %r{<age type="integer">25</age>}, @contact.to_xml - end - - def test_should_serialize_binary - xml = @contact.to_xml - assert_match %r{YmluYXJ5ZGF0YQ==\n</avatar>}, xml - assert_match %r{<avatar(.*)(type="binary")}, xml - assert_match %r{<avatar(.*)(encoding="base64")}, xml - end - - def test_should_serialize_datetime - assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml - end - - def test_should_serialize_boolean - assert_match %r{<awesome type=\"boolean\">false</awesome>}, @contact.to_xml - end - - def test_should_serialize_hash - assert_match %r{<preferences>\s*<gem>ruby</gem>\s*</preferences>}m, @contact.to_xml - end - - def test_uses_serializable_hash_with_only_option - def @contact.serializable_hash(options=nil) - super(only: %w(name)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{age}, xml - assert_no_match %r{awesome}, xml - end - - def test_uses_serializable_hash_with_except_option - def @contact.serializable_hash(options=nil) - super(except: %w(age)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_match %r{<awesome type=\"boolean\">false</awesome>}, xml - assert_no_match %r{age}, xml - end - - def test_does_not_include_inheritance_column_from_sti - @contact = ContactSti.new(@contact.attributes) - assert_equal 'ContactSti', @contact.type - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{<type}, xml - assert_no_match %r{ContactSti}, xml - end - - def test_serializable_hash_with_default_except_option_and_excluding_inheritance_column_from_sti - @contact = ContactSti.new(@contact.attributes) - assert_equal 'ContactSti', @contact.type - - def @contact.serializable_hash(options={}) - super({ except: %w(age) }.merge!(options)) - end - - xml = @contact.to_xml - assert_match %r{<name>aaron stack</name>}, xml - assert_no_match %r{age}, xml - assert_no_match %r{<type}, xml - assert_no_match %r{ContactSti}, xml - end -end - -class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase - def test_should_serialize_datetime_with_timezone - with_timezone_config zone: "Pacific Time (US & Canada)" do - toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1)) - assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml - end - end - - def test_should_serialize_datetime_with_timezone_reloaded - with_timezone_config zone: "Pacific Time (US & Canada)" do - toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload - assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml - end - end -end - -class NilXmlSerializationTest < ActiveRecord::TestCase - def setup - @xml = Contact.new.to_xml(:root => 'xml_contact') - end - - def test_should_serialize_string - assert_match %r{<name nil="true"/>}, @xml - end - - def test_should_serialize_integer - assert %r{<age (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{nil="true"}, attributes - assert_match %r{type="integer"}, attributes - end - - def test_should_serialize_binary - assert %r{<avatar (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{type="binary"}, attributes - assert_match %r{encoding="base64"}, attributes - assert_match %r{nil="true"}, attributes - end - - def test_should_serialize_datetime - assert %r{<created-at (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{nil="true"}, attributes - assert_match %r{type="dateTime"}, attributes - end - - def test_should_serialize_boolean - assert %r{<awesome (.*)/>}.match(@xml) - attributes = $1 - assert_match %r{type="boolean"}, attributes - assert_match %r{nil="true"}, attributes - end - - def test_should_serialize_yaml - assert_match %r{<preferences nil=\"true\"/>}, @xml - end -end - -class DatabaseConnectedXmlSerializationTest < ActiveRecord::TestCase - fixtures :topics, :companies, :accounts, :authors, :posts, :projects - - def test_to_xml - xml = REXML::Document.new(topics(:first).to_xml(:indent => 0)) - bonus_time_in_current_timezone = topics(:first).bonus_time.xmlschema - written_on_in_current_timezone = topics(:first).written_on.xmlschema - - assert_equal "topic", xml.root.name - assert_equal "The First Topic" , xml.elements["//title"].text - assert_equal "David" , xml.elements["//author-name"].text - assert_match "Have a nice day", xml.elements["//content"].text - - assert_equal "1", xml.elements["//id"].text - assert_equal "integer" , xml.elements["//id"].attributes['type'] - - assert_equal "1", xml.elements["//replies-count"].text - assert_equal "integer" , xml.elements["//replies-count"].attributes['type'] - - assert_equal written_on_in_current_timezone, xml.elements["//written-on"].text - assert_equal "dateTime" , xml.elements["//written-on"].attributes['type'] - - assert_equal "david@loudthinking.com", xml.elements["//author-email-address"].text - - assert_equal nil, xml.elements["//parent-id"].text - assert_equal "integer", xml.elements["//parent-id"].attributes['type'] - assert_equal "true", xml.elements["//parent-id"].attributes['nil'] - - # Oracle enhanced adapter allows to define Date attributes in model class (see topic.rb) - assert_equal "2004-04-15", xml.elements["//last-read"].text - assert_equal "date" , xml.elements["//last-read"].attributes['type'] - - # Oracle and DB2 don't have true boolean or time-only fields - unless current_adapter?(:OracleAdapter, :DB2Adapter) - assert_equal "false", xml.elements["//approved"].text - assert_equal "boolean" , xml.elements["//approved"].attributes['type'] - - assert_equal bonus_time_in_current_timezone, xml.elements["//bonus-time"].text - assert_equal "dateTime" , xml.elements["//bonus-time"].attributes['type'] - end - end - - def test_except_option - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :replies_count]) - assert_equal "<topic>", xml.first(7) - assert !xml.include?(%(<title>The First Topic</title>)) - assert xml.include?(%(<author-name>David</author-name>)) - - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :except => [:title, :author_name, :replies_count]) - assert !xml.include?(%(<title>The First Topic</title>)) - assert !xml.include?(%(<author-name>David</author-name>)) - end - - # to_xml used to mess with the hash the user provided which - # caused the builder to be reused. This meant the document kept - # getting appended to. - - def test_modules - projects = MyApplication::Business::Project.all - xml = projects.to_xml - root = projects.first.class.to_s.underscore.pluralize.tr('/','_').dasherize - assert_match "<#{root} type=\"array\">", xml - assert_match "</#{root}>", xml - end - - def test_passing_hash_shouldnt_reuse_builder - options = {:include=>:posts} - david = authors(:david) - first_xml_size = david.to_xml(options).size - second_xml_size = david.to_xml(options).size - assert_equal first_xml_size, second_xml_size - end - - def test_include_uses_association_name - xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0 - assert_match %r{<hello-posts type="array">}, xml - assert_match %r{<hello-post type="Post">}, xml - assert_match %r{<hello-post type="StiPost">}, xml - end - - def test_included_associations_should_skip_types - xml = authors(:david).to_xml :include=>:hello_posts, :indent => 0, :skip_types => true - assert_match %r{<hello-posts>}, xml - assert_match %r{<hello-post>}, xml - assert_match %r{<hello-post>}, xml - end - - def test_including_has_many_association - xml = topics(:first).to_xml(:indent => 0, :skip_instruct => true, :include => :replies, :except => :replies_count) - assert_equal "<topic>", xml.first(7) - assert xml.include?(%(<replies type="array"><reply>)) - assert xml.include?(%(<title>The Second Topic of the day</title>)) - end - - def test_including_belongs_to_association - xml = companies(:first_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert !xml.include?("<firm>") - - xml = companies(:second_client).to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert xml.include?("<firm>") - end - - def test_including_multiple_associations - xml = companies(:first_firm).to_xml(:indent => 0, :skip_instruct => true, :include => [ :clients, :account ]) - assert_equal "<firm>", xml.first(6) - assert xml.include?(%(<account>)) - assert xml.include?(%(<clients type="array"><client>)) - end - - def test_including_association_with_options - xml = companies(:first_firm).to_xml( - :indent => 0, :skip_instruct => true, - :include => { :clients => { :only => :name } } - ) - - assert_equal "<firm>", xml.first(6) - assert xml.include?(%(<client><name>Summit</name></client>)) - assert xml.include?(%(<clients type="array"><client>)) - end - - def test_methods_are_called_on_object - xml = authors(:david).to_xml :methods => :label, :indent => 0 - assert_match %r{<label>.*</label>}, xml - end - - def test_should_not_call_methods_on_associations_that_dont_respond - xml = authors(:david).to_xml :include=>:hello_posts, :methods => :label, :indent => 2 - assert !authors(:david).hello_posts.first.respond_to?(:label) - assert_match %r{^ <label>.*</label>}, xml - assert_no_match %r{^ <label>}, xml - end - - def test_procs_are_called_on_object - proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') } - xml = authors(:david).to_xml(:procs => [ proc ]) - assert_match %r{<nationality>Danish</nationality>}, xml - end - - def test_dual_arity_procs_are_called_on_object - proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) } - xml = authors(:david).to_xml(:procs => [ proc ]) - assert_match %r{<name-reverse>divaD</name-reverse>}, xml - end - - def test_top_level_procs_arent_applied_to_associations - author_proc = Proc.new { |options| options[:builder].tag!('nationality', 'Danish') } - xml = authors(:david).to_xml(:procs => [ author_proc ], :include => :posts, :indent => 2) - - assert_match %r{^ <nationality>Danish</nationality>}, xml - assert_no_match %r{^ {6}<nationality>Danish</nationality>}, xml - end - - def test_procs_on_included_associations_are_called - posts_proc = Proc.new { |options| options[:builder].tag!('copyright', 'DHH') } - xml = authors(:david).to_xml( - :indent => 2, - :include => { - :posts => { :procs => [ posts_proc ] } - } - ) - - assert_no_match %r{^ <copyright>DHH</copyright>}, xml - assert_match %r{^ {6}<copyright>DHH</copyright>}, xml - end - - def test_should_include_empty_has_many_as_empty_array - authors(:david).posts.delete_all - xml = authors(:david).to_xml :include=>:posts, :indent => 2 - - assert_equal [], Hash.from_xml(xml)['author']['posts'] - assert_match %r{^ <posts type="array"/>}, xml - end - - def test_should_has_many_array_elements_should_include_type_when_different_from_guessed_value - xml = authors(:david).to_xml :include=>:posts_with_comments, :indent => 2 - - assert Hash.from_xml(xml) - assert_match %r{^ <posts-with-comments type="array">}, xml - assert_match %r{^ <posts-with-comment type="Post">}, xml - assert_match %r{^ <posts-with-comment type="StiPost">}, xml - - types = Hash.from_xml(xml)['author']['posts_with_comments'].collect {|t| t['type'] } - assert types.include?('SpecialPost') - assert types.include?('Post') - assert types.include?('StiPost') - end - - def test_should_produce_xml_for_methods_returning_array - xml = authors(:david).to_xml(:methods => :social) - array = Hash.from_xml(xml)['author']['social'] - assert_equal 2, array.size - assert array.include? 'twitter' - assert array.include? 'github' - end - - def test_should_support_aliased_attributes - xml = Author.select("name as firstname").to_xml - Author.all.each do |author| - assert xml.include?(%(<firstname>#{author.name}</firstname>)), xml - end - end - - def test_array_to_xml_including_has_many_association - xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :include => :replies) - assert xml.include?(%(<replies type="array"><reply>)) - end - - def test_array_to_xml_including_methods - xml = [ topics(:first), topics(:second) ].to_xml(:indent => 0, :skip_instruct => true, :methods => [ :topic_id ]) - assert xml.include?(%(<topic-id type="integer">#{topics(:first).topic_id}</topic-id>)), xml - assert xml.include?(%(<topic-id type="integer">#{topics(:second).topic_id}</topic-id>)), xml - end - - def test_array_to_xml_including_has_one_association - xml = [ companies(:first_firm), companies(:rails_core) ].to_xml(:indent => 0, :skip_instruct => true, :include => :account) - assert xml.include?(companies(:first_firm).account.to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:rails_core).account.to_xml(:indent => 0, :skip_instruct => true)) - end - - def test_array_to_xml_including_belongs_to_association - xml = [ companies(:first_client), companies(:second_client), companies(:another_client) ].to_xml(:indent => 0, :skip_instruct => true, :include => :firm) - assert xml.include?(companies(:first_client).to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:second_client).firm.to_xml(:indent => 0, :skip_instruct => true)) - assert xml.include?(companies(:another_client).firm.to_xml(:indent => 0, :skip_instruct => true)) - end -end |