diff options
Diffstat (limited to 'activerecord/test/cases')
62 files changed, 1840 insertions, 512 deletions
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 64c2b51f83..e40ae3090c 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -109,6 +109,11 @@ module ActiveRecord end end + def test_exec_query_returns_an_empty_result + result = @connection.exec_query "INSERT INTO subscribers(nick) VALUES('me')" + assert_instance_of(ActiveRecord::Result, result) + end + if current_adapter?(:Mysql2Adapter) def test_charset assert_not_nil @connection.charset @@ -286,18 +291,6 @@ module ActiveRecord assert_equal "special_db_type", @connection.type_to_sql(:special_db_type) end - unless current_adapter?(:PostgreSQLAdapter) - def test_log_invalid_encoding - error = assert_raises RuntimeError do - @connection.send :log, "SELECT 'ы' FROM DUAL" do - raise (+"ы").force_encoding(Encoding::ASCII_8BIT) - end - end - - assert_equal "ы", error.message - end - end - def test_supports_multi_insert_is_deprecated assert_deprecated { @connection.supports_multi_insert? } end diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb index 0719baaa23..7bc86476b6 100644 --- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb @@ -69,6 +69,64 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase @conn.exec_query("ALTER TABLE engines DROP COLUMN old_car_id") end + def test_errors_when_an_insert_query_is_called_while_preventing_writes + assert_raises(ActiveRecord::ReadOnlyError) do + @conn.while_preventing_writes do + @conn.insert("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + end + end + end + + def test_errors_when_an_update_query_is_called_while_preventing_writes + @conn.insert("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @conn.while_preventing_writes do + @conn.update("UPDATE `engines` SET `engines`.`car_id` = '9989' WHERE `engines`.`car_id` = '138853948594'") + end + end + end + + def test_errors_when_a_delete_query_is_called_while_preventing_writes + @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @conn.while_preventing_writes do + @conn.execute("DELETE FROM `engines` where `engines`.`car_id` = '138853948594'") + end + end + end + + def test_errors_when_a_replace_query_is_called_while_preventing_writes + @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @conn.while_preventing_writes do + @conn.execute("REPLACE INTO `engines` SET `engines`.`car_id` = '249823948'") + end + end + end + + def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes + @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')") + + @conn.while_preventing_writes do + assert_equal 1, @conn.execute("SELECT `engines`.* FROM `engines` WHERE `engines`.`car_id` = '138853948594'").entries.count + end + end + + def test_doesnt_error_when_a_show_query_is_called_while_preventing_writes + @conn.while_preventing_writes do + assert_equal 2, @conn.execute("SHOW FULL FIELDS FROM `engines`").entries.count + end + end + + def test_doesnt_error_when_a_set_query_is_called_while_preventing_writes + @conn.while_preventing_writes do + assert_nil @conn.execute("SET NAMES utf8") + end + end + private def with_example_table(definition = "id int auto_increment primary key, number int, data varchar(255)", &block) diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index b0ce2694a3..683066cdb3 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -15,13 +15,13 @@ module PostgresqlCompositeBehavior @connection = ActiveRecord::Base.connection @connection.transaction do - @connection.execute <<-SQL - CREATE TYPE full_address AS - ( - city VARCHAR(90), - street VARCHAR(90) - ); - SQL + @connection.execute <<~SQL + CREATE TYPE full_address AS + ( + city VARCHAR(90), + street VARCHAR(90) + ); + SQL @connection.create_table("postgresql_composites") do |t| t.column :address, :full_address end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 70aa189893..40ab158c05 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -146,34 +146,15 @@ module ActiveRecord end end - # Must have PostgreSQL >= 9.2, or with_manual_interventions set to - # true for this test to run. - # - # When prompted, restart the PostgreSQL server with the - # "-m fast" option or kill the individual connection assuming - # you know the incantation to do that. - # To restart PostgreSQL 9.1 on macOS, installed via MacPorts, ... - # sudo su postgres -c "pg_ctl restart -D /opt/local/var/db/postgresql91/defaultdb/ -m fast" def test_reconnection_after_actual_disconnection_with_verify original_connection_pid = @connection.query("select pg_backend_pid()") # Sanity check. assert_predicate @connection, :active? - if @connection.send(:postgresql_version) >= 90200 - secondary_connection = ActiveRecord::Base.connection_pool.checkout - secondary_connection.query("select pg_terminate_backend(#{original_connection_pid.first.first})") - ActiveRecord::Base.connection_pool.checkin(secondary_connection) - elsif ARTest.config["with_manual_interventions"] - puts "Kill the connection now (e.g. by restarting the PostgreSQL " \ - 'server with the "-m fast" option) and then press enter.' - $stdin.gets - else - # We're not capable of terminating the backend ourselves, and - # we're not allowed to seek assistance; bail out without - # actually testing anything. - return - end + secondary_connection = ActiveRecord::Base.connection_pool.checkout + secondary_connection.query("select pg_terminate_backend(#{original_connection_pid.first.first})") + ActiveRecord::Base.connection_pool.checkin(secondary_connection) @connection.verify! @@ -230,7 +211,7 @@ module ActiveRecord def test_get_and_release_advisory_lock lock_id = 5295901941911233559 - list_advisory_locks = <<-SQL + list_advisory_locks = <<~SQL SELECT locktype, (classid::bigint << 32) | objid::bigint AS lock_id FROM pg_locks @@ -261,6 +242,10 @@ module ActiveRecord end end + def test_supports_ranges_is_deprecated + assert_deprecated { @connection.supports_ranges? } + end + private def with_warning_suppression diff --git a/activerecord/test/cases/adapters/postgresql/create_unlogged_tables_test.rb b/activerecord/test/cases/adapters/postgresql/create_unlogged_tables_test.rb new file mode 100644 index 0000000000..a02bae1453 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/create_unlogged_tables_test.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require "cases/helper" +require "support/schema_dumping_helper" + +class UnloggedTablesTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + TABLE_NAME = "things" + LOGGED_FIELD = "relpersistence" + LOGGED_QUERY = "SELECT #{LOGGED_FIELD} FROM pg_class WHERE relname = '#{TABLE_NAME}'" + LOGGED = "p" + UNLOGGED = "u" + TEMPORARY = "t" + + class Thing < ActiveRecord::Base + self.table_name = TABLE_NAME + end + + def setup + @connection = ActiveRecord::Base.connection + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = false + end + + teardown do + @connection.drop_table TABLE_NAME, if_exists: true + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = false + end + + def test_logged_by_default + @connection.create_table(TABLE_NAME) do |t| + end + assert_equal @connection.execute(LOGGED_QUERY).first[LOGGED_FIELD], LOGGED + end + + def test_unlogged_in_test_environment_when_unlogged_setting_enabled + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true + + @connection.create_table(TABLE_NAME) do |t| + end + assert_equal @connection.execute(LOGGED_QUERY).first[LOGGED_FIELD], UNLOGGED + end + + def test_not_included_in_schema_dump + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true + + @connection.create_table(TABLE_NAME) do |t| + end + assert_no_match(/unlogged/i, dump_table_schema(TABLE_NAME)) + end + + def test_not_changed_in_change_table + @connection.create_table(TABLE_NAME) do |t| + end + + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.create_unlogged_tables = true + + @connection.change_table(TABLE_NAME) do |t| + t.column :name, :string + end + assert_equal @connection.execute(LOGGED_QUERY).first[LOGGED_FIELD], LOGGED + end + + def test_gracefully_handles_temporary_tables + @connection.create_table(TABLE_NAME, temporary: true) do |t| + end + + # Temporary tables are already unlogged, though this query results in a + # different result ("t" vs. "u"). This test is really just checking that we + # didn't try to run `CREATE TEMPORARY UNLOGGED TABLE`, which would result in + # a PostgreSQL error. + assert_equal @connection.execute(LOGGED_QUERY).first[LOGGED_FIELD], TEMPORARY + end +end diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index 6789ff63e7..416a2b141b 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -13,7 +13,7 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase def setup @connection = ActiveRecord::Base.connection @connection.transaction do - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE TYPE mood AS ENUM ('sad', 'ok', 'happy'); SQL @connection.create_table("postgresql_enums") do |t| diff --git a/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb index 4fa315ad23..69339c8a31 100644 --- a/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb +++ b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb @@ -22,18 +22,18 @@ if ActiveRecord::Base.connection.supports_foreign_tables? enable_extension!("postgres_fdw", @connection) foreign_db_config = ARTest.connection_config["arunit2"] - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE SERVER foreign_server FOREIGN DATA WRAPPER postgres_fdw OPTIONS (dbname '#{foreign_db_config["database"]}') SQL - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE USER MAPPING FOR CURRENT_USER SERVER foreign_server SQL - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE FOREIGN TABLE foreign_professors ( id int, name character varying NOT NULL @@ -45,7 +45,7 @@ if ActiveRecord::Base.connection.supports_foreign_tables? def teardown disable_extension!("postgres_fdw", @connection) - @connection.execute <<-SQL + @connection.execute <<~SQL DROP SERVER IF EXISTS foreign_server CASCADE SQL end diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index 75e5aaed53..1aa0348879 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -37,7 +37,7 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase def test_default assert_equal BigDecimal("150.55"), PostgresqlMoney.column_defaults["depth"] assert_equal BigDecimal("150.55"), PostgresqlMoney.new.depth - assert_equal "$150.55", PostgresqlMoney.new.depth_before_type_cast + assert_equal "150.55", PostgresqlMoney.new.depth_before_type_cast end def test_money_values diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index cbb6cd42b5..34b4fc344b 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -376,6 +376,62 @@ module ActiveRecord end end + def test_errors_when_an_insert_query_is_called_while_preventing_writes + with_example_table do + assert_raises(ActiveRecord::ReadOnlyError) do + @connection.while_preventing_writes do + @connection.execute("INSERT INTO ex (data) VALUES ('138853948594')") + end + end + end + end + + def test_errors_when_an_update_query_is_called_while_preventing_writes + with_example_table do + @connection.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @connection.while_preventing_writes do + @connection.execute("UPDATE ex SET data = '9989' WHERE data = '138853948594'") + end + end + end + end + + def test_errors_when_a_delete_query_is_called_while_preventing_writes + with_example_table do + @connection.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @connection.while_preventing_writes do + @connection.execute("DELETE FROM ex where data = '138853948594'") + end + end + end + end + + def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes + with_example_table do + @connection.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + @connection.while_preventing_writes do + assert_equal 1, @connection.execute("SELECT * FROM ex WHERE data = '138853948594'").entries.count + end + end + end + + def test_doesnt_error_when_a_show_query_is_called_while_preventing_writes + @connection.while_preventing_writes do + assert_equal 1, @connection.execute("SHOW TIME ZONE").entries.count + end + end + + def test_doesnt_error_when_a_set_query_is_called_while_preventing_writes + @connection.while_preventing_writes do + assert_equal [], @connection.execute("SET standard_conforming_strings = on").entries + end + end + private def with_example_table(definition = "id serial primary key, number integer, data character varying(255)", &block) diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index d50dc49276..d571355a9c 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -39,6 +39,11 @@ module ActiveRecord type = OID::Bit.new assert_nil @conn.quote(type.serialize(value)) end + + def test_quote_table_name_with_spaces + value = "user posts" + assert_equal "\"user posts\"", @conn.quote_table_name(value) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 433598500d..478cd5aa76 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -3,418 +3,416 @@ require "cases/helper" require "support/connection_helper" -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::PostgreSQLTestCase - self.use_transactional_tests = false - include ConnectionHelper - include InTimeZone - - def setup - @connection = PostgresqlRange.connection - begin - @connection.transaction do - @connection.execute <<_SQL - CREATE TYPE floatrange AS RANGE ( - subtype = float8, - subtype_diff = float8mi - ); -_SQL - - @connection.create_table("postgresql_ranges") do |t| - t.daterange :date_range - t.numrange :num_range - t.tsrange :ts_range - t.tstzrange :tstz_range - t.int4range :int4_range - t.int8range :int8_range - end - - @connection.add_column "postgresql_ranges", "float_range", "floatrange" - end - PostgresqlRange.reset_column_information - rescue ActiveRecord::StatementInvalid - skip "do not test on PG without range" - end - - insert_range(id: 101, - date_range: "[''2012-01-02'', ''2012-01-04'']", - num_range: "[0.1, 0.2]", - ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']", - tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']", - int4_range: "[1, 10]", - int8_range: "[10, 100]", - float_range: "[0.5, 0.7]") - - insert_range(id: 102, - date_range: "[''2012-01-02'', ''2012-01-04'')", - num_range: "[0.1, 0.2)", - ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')", - tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')", - int4_range: "[1, 10)", - int8_range: "[10, 100)", - float_range: "[0.5, 0.7)") - - insert_range(id: 103, - date_range: "[''2012-01-02'',]", - num_range: "[0.1,]", - ts_range: "[''2010-01-01 14:30'',]", - tstz_range: "[''2010-01-01 14:30:00+05'',]", - int4_range: "[1,]", - int8_range: "[10,]", - float_range: "[0.5,]") - - insert_range(id: 104, - date_range: "[,]", - num_range: "[,]", - ts_range: "[,]", - tstz_range: "[,]", - int4_range: "[,]", - int8_range: "[,]", - float_range: "[,]") - - insert_range(id: 105, - date_range: "[''2012-01-02'', ''2012-01-02'')", - num_range: "[0.1, 0.1)", - ts_range: "[''2010-01-01 14:30'', ''2010-01-01 14:30'')", - tstz_range: "[''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')", - int4_range: "[1, 1)", - int8_range: "[10, 10)", - float_range: "[0.5, 0.5)") - - @new_range = PostgresqlRange.new - @first_range = PostgresqlRange.find(101) - @second_range = PostgresqlRange.find(102) - @third_range = PostgresqlRange.find(103) - @fourth_range = PostgresqlRange.find(104) - @empty_range = PostgresqlRange.find(105) - end +class PostgresqlRange < ActiveRecord::Base + self.table_name = "postgresql_ranges" + self.time_zone_aware_types += [:tsrange, :tstzrange] +end - teardown do - @connection.drop_table "postgresql_ranges", if_exists: true - @connection.execute "DROP TYPE IF EXISTS floatrange" - reset_connection - end +class PostgresqlRangeTest < ActiveRecord::PostgreSQLTestCase + self.use_transactional_tests = false + include ConnectionHelper + include InTimeZone + + def setup + @connection = PostgresqlRange.connection + begin + @connection.transaction do + @connection.execute <<~SQL + CREATE TYPE floatrange AS RANGE ( + subtype = float8, + subtype_diff = float8mi + ); + SQL - def test_data_type_of_range_types - assert_equal :daterange, @first_range.column_for_attribute(:date_range).type - assert_equal :numrange, @first_range.column_for_attribute(:num_range).type - assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type - assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type - assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type - assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type - end + @connection.create_table("postgresql_ranges") do |t| + t.daterange :date_range + t.numrange :num_range + t.tsrange :ts_range + t.tstzrange :tstz_range + t.int4range :int4_range + t.int8range :int8_range + end - def test_int4range_values - assert_equal 1...11, @first_range.int4_range - assert_equal 1...10, @second_range.int4_range - assert_equal 1...Float::INFINITY, @third_range.int4_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) - assert_nil @empty_range.int4_range - end + @connection.add_column "postgresql_ranges", "float_range", "floatrange" + end + PostgresqlRange.reset_column_information + rescue ActiveRecord::StatementInvalid + skip "do not test on PG without range" + end + + insert_range(id: 101, + date_range: "[''2012-01-02'', ''2012-01-04'']", + num_range: "[0.1, 0.2]", + ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'']", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']", + int4_range: "[1, 10]", + int8_range: "[10, 100]", + float_range: "[0.5, 0.7]") + + insert_range(id: 102, + date_range: "[''2012-01-02'', ''2012-01-04'')", + num_range: "[0.1, 0.2)", + ts_range: "[''2010-01-01 14:30'', ''2011-01-01 14:30'')", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')", + int4_range: "[1, 10)", + int8_range: "[10, 100)", + float_range: "[0.5, 0.7)") + + insert_range(id: 103, + date_range: "[''2012-01-02'',]", + num_range: "[0.1,]", + ts_range: "[''2010-01-01 14:30'',]", + tstz_range: "[''2010-01-01 14:30:00+05'',]", + int4_range: "[1,]", + int8_range: "[10,]", + float_range: "[0.5,]") + + insert_range(id: 104, + date_range: "[,]", + num_range: "[,]", + ts_range: "[,]", + tstz_range: "[,]", + int4_range: "[,]", + int8_range: "[,]", + float_range: "[,]") + + insert_range(id: 105, + date_range: "[''2012-01-02'', ''2012-01-02'')", + num_range: "[0.1, 0.1)", + ts_range: "[''2010-01-01 14:30'', ''2010-01-01 14:30'')", + tstz_range: "[''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')", + int4_range: "[1, 1)", + int8_range: "[10, 10)", + float_range: "[0.5, 0.5)") + + @new_range = PostgresqlRange.new + @first_range = PostgresqlRange.find(101) + @second_range = PostgresqlRange.find(102) + @third_range = PostgresqlRange.find(103) + @fourth_range = PostgresqlRange.find(104) + @empty_range = PostgresqlRange.find(105) + end - def test_int8range_values - assert_equal 10...101, @first_range.int8_range - assert_equal 10...100, @second_range.int8_range - assert_equal 10...Float::INFINITY, @third_range.int8_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) - assert_nil @empty_range.int8_range - end + teardown do + @connection.drop_table "postgresql_ranges", if_exists: true + @connection.execute "DROP TYPE IF EXISTS floatrange" + reset_connection + end - def test_daterange_values - assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range - assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 4), @second_range.date_range - assert_equal Date.new(2012, 1, 2)...Float::INFINITY, @third_range.date_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) - assert_nil @empty_range.date_range - end + def test_data_type_of_range_types + assert_equal :daterange, @first_range.column_for_attribute(:date_range).type + assert_equal :numrange, @first_range.column_for_attribute(:num_range).type + assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type + assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type + assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type + assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type + end - def test_numrange_values - assert_equal BigDecimal("0.1")..BigDecimal("0.2"), @first_range.num_range - assert_equal BigDecimal("0.1")...BigDecimal("0.2"), @second_range.num_range - assert_equal BigDecimal("0.1")...BigDecimal("Infinity"), @third_range.num_range - assert_equal BigDecimal("-Infinity")...BigDecimal("Infinity"), @fourth_range.num_range - assert_nil @empty_range.num_range - end + def test_int4range_values + assert_equal 1...11, @first_range.int4_range + assert_equal 1...10, @second_range.int4_range + assert_equal 1...Float::INFINITY, @third_range.int4_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range) + assert_nil @empty_range.int4_range + end - def test_tsrange_values - tz = ::ActiveRecord::Base.default_timezone - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range - assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) - assert_nil @empty_range.ts_range - end + def test_int8range_values + assert_equal 10...101, @first_range.int8_range + assert_equal 10...100, @second_range.int8_range + assert_equal 10...Float::INFINITY, @third_range.int8_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range) + assert_nil @empty_range.int8_range + end - def test_tstzrange_values - assert_equal Time.parse("2010-01-01 09:30:00 UTC")..Time.parse("2011-01-01 17:30:00 UTC"), @first_range.tstz_range - assert_equal Time.parse("2010-01-01 09:30:00 UTC")...Time.parse("2011-01-01 17:30:00 UTC"), @second_range.tstz_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) - assert_nil @empty_range.tstz_range - end + def test_daterange_values + assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range + assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 4), @second_range.date_range + assert_equal Date.new(2012, 1, 2)...Float::INFINITY, @third_range.date_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range) + assert_nil @empty_range.date_range + end - def test_custom_range_values - assert_equal 0.5..0.7, @first_range.float_range - assert_equal 0.5...0.7, @second_range.float_range - assert_equal 0.5...Float::INFINITY, @third_range.float_range - assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.float_range) - assert_nil @empty_range.float_range - end + def test_numrange_values + assert_equal BigDecimal("0.1")..BigDecimal("0.2"), @first_range.num_range + assert_equal BigDecimal("0.1")...BigDecimal("0.2"), @second_range.num_range + assert_equal BigDecimal("0.1")...BigDecimal("Infinity"), @third_range.num_range + assert_equal BigDecimal("-Infinity")...BigDecimal("Infinity"), @fourth_range.num_range + assert_nil @empty_range.num_range + end - def test_timezone_awareness_tzrange - tz = "Pacific Time (US & Canada)" + def test_tsrange_values + tz = ::ActiveRecord::Base.default_timezone + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range + assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range) + assert_nil @empty_range.ts_range + end - in_time_zone tz do - PostgresqlRange.reset_column_information - time_string = Time.current.to_s - time = Time.zone.parse(time_string) + def test_tstzrange_values + assert_equal Time.parse("2010-01-01 09:30:00 UTC")..Time.parse("2011-01-01 17:30:00 UTC"), @first_range.tstz_range + assert_equal Time.parse("2010-01-01 09:30:00 UTC")...Time.parse("2011-01-01 17:30:00 UTC"), @second_range.tstz_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range) + assert_nil @empty_range.tstz_range + end - 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 + def test_custom_range_values + assert_equal 0.5..0.7, @first_range.float_range + assert_equal 0.5...0.7, @second_range.float_range + assert_equal 0.5...Float::INFINITY, @third_range.float_range + assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.float_range) + assert_nil @empty_range.float_range + end - record.save! - record.reload + def test_timezone_awareness_tzrange + tz = "Pacific Time (US & Canada)" - assert_equal time..time, record.tstz_range - assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone - end - end + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) - 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) - assert_equal @new_range.tstz_range, tstzrange - assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00 UTC")...Time.parse("2011-02-02 19:30:00 UTC") - end + 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 - def test_update_tstzrange - assert_equal_round_trip(@first_range, :tstz_range, - Time.parse("2010-01-01 14:30:00 CDT")...Time.parse("2011-02-02 14:30:00 CET")) - assert_nil_round_trip(@first_range, :tstz_range, - Time.parse("2010-01-01 14:30:00 +0100")...Time.parse("2010-01-01 13:30:00 +0000")) - end + record.save! + record.reload - def test_create_tsrange - tz = ::ActiveRecord::Base.default_timezone - assert_equal_round_trip(@new_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) + assert_equal time..time, record.tstz_range + assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone end + end - def test_update_tsrange - tz = ::ActiveRecord::Base.default_timezone - assert_equal_round_trip(@first_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) - assert_nil_round_trip(@first_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0)) - 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) + assert_equal @new_range.tstz_range, tstzrange + assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00 UTC")...Time.parse("2011-02-02 19:30:00 UTC") + end - def test_timezone_awareness_tsrange - tz = "Pacific Time (US & Canada)" + def test_update_tstzrange + assert_equal_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00 CDT")...Time.parse("2011-02-02 14:30:00 CET")) + assert_nil_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00 +0100")...Time.parse("2010-01-01 13:30:00 +0000")) + end - in_time_zone tz do - PostgresqlRange.reset_column_information - time_string = Time.current.to_s - time = Time.zone.parse(time_string) + def test_create_tsrange + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@new_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) + end - 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 + def test_update_tsrange + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)) + assert_nil_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0)) + end - record.save! - record.reload + def test_timezone_awareness_tsrange + tz = "Pacific Time (US & Canada)" - assert_equal time..time, record.ts_range - assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone - end - end + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) - def test_create_tstzrange_preserve_usec - tstzrange = Time.parse("2010-01-01 14:30:00.670277 +0100")...Time.parse("2011-02-02 14:30:00.745125 CDT") - round_trip(@new_range, :tstz_range, tstzrange) - assert_equal @new_range.tstz_range, tstzrange - assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00.670277 UTC")...Time.parse("2011-02-02 19:30:00.745125 UTC") - end + 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 - def test_update_tstzrange_preserve_usec - assert_equal_round_trip(@first_range, :tstz_range, - Time.parse("2010-01-01 14:30:00.245124 CDT")...Time.parse("2011-02-02 14:30:00.451274 CET")) - assert_nil_round_trip(@first_range, :tstz_range, - Time.parse("2010-01-01 14:30:00.245124 +0100")...Time.parse("2010-01-01 13:30:00.245124 +0000")) - end + record.save! + record.reload - def test_create_tsrange_preseve_usec - tz = ::ActiveRecord::Base.default_timezone - assert_equal_round_trip(@new_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0, 125435)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 225435)) + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone end + end - def test_update_tsrange_preserve_usec - tz = ::ActiveRecord::Base.default_timezone - assert_equal_round_trip(@first_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 224242)) - assert_nil_round_trip(@first_range, :ts_range, - Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)) - end + def test_create_tstzrange_preserve_usec + tstzrange = Time.parse("2010-01-01 14:30:00.670277 +0100")...Time.parse("2011-02-02 14:30:00.745125 CDT") + round_trip(@new_range, :tstz_range, tstzrange) + assert_equal @new_range.tstz_range, tstzrange + assert_equal @new_range.tstz_range, Time.parse("2010-01-01 13:30:00.670277 UTC")...Time.parse("2011-02-02 19:30:00.745125 UTC") + end - def test_timezone_awareness_tsrange_preserve_usec - tz = "Pacific Time (US & Canada)" + def test_update_tstzrange_preserve_usec + assert_equal_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00.245124 CDT")...Time.parse("2011-02-02 14:30:00.451274 CET")) + assert_nil_round_trip(@first_range, :tstz_range, + Time.parse("2010-01-01 14:30:00.245124 +0100")...Time.parse("2010-01-01 13:30:00.245124 +0000")) + end - in_time_zone tz do - PostgresqlRange.reset_column_information - time_string = "2017-09-26 07:30:59.132451 -0700" - time = Time.zone.parse(time_string) - assert time.usec > 0 + def test_create_tsrange_preseve_usec + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@new_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 125435)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 225435)) + end - 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 - assert_equal time.usec, record.ts_range.begin.usec + def test_update_tsrange_preserve_usec + tz = ::ActiveRecord::Base.default_timezone + assert_equal_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2011, 2, 2, 14, 30, 0, 224242)) + assert_nil_round_trip(@first_range, :ts_range, + Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)...Time.send(tz, 2010, 1, 1, 14, 30, 0, 142432)) + end - record.save! - record.reload + def test_timezone_awareness_tsrange_preserve_usec + tz = "Pacific Time (US & Canada)" - assert_equal time..time, record.ts_range - assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone - assert_equal time.usec, record.ts_range.begin.usec - end - end + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = "2017-09-26 07:30:59.132451 -0700" + time = Time.zone.parse(time_string) + assert time.usec > 0 - def test_create_numrange - assert_equal_round_trip(@new_range, :num_range, - BigDecimal("0.5")...BigDecimal("1")) - end + 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 + assert_equal time.usec, record.ts_range.begin.usec - def test_update_numrange - assert_equal_round_trip(@first_range, :num_range, - BigDecimal("0.5")...BigDecimal("1")) - assert_nil_round_trip(@first_range, :num_range, - BigDecimal("0.5")...BigDecimal("0.5")) - end + record.save! + record.reload - def test_create_daterange - assert_equal_round_trip(@new_range, :date_range, - Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true)) + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + assert_equal time.usec, record.ts_range.begin.usec end + end - def test_update_daterange - assert_equal_round_trip(@first_range, :date_range, - Date.new(2012, 2, 3)...Date.new(2012, 2, 10)) - assert_nil_round_trip(@first_range, :date_range, - Date.new(2012, 2, 3)...Date.new(2012, 2, 3)) - end + def test_create_numrange + assert_equal_round_trip(@new_range, :num_range, + BigDecimal("0.5")...BigDecimal("1")) + end - def test_create_int4range - assert_equal_round_trip(@new_range, :int4_range, Range.new(3, 50, true)) - end + def test_update_numrange + assert_equal_round_trip(@first_range, :num_range, + BigDecimal("0.5")...BigDecimal("1")) + assert_nil_round_trip(@first_range, :num_range, + BigDecimal("0.5")...BigDecimal("0.5")) + end - def test_update_int4range - assert_equal_round_trip(@first_range, :int4_range, 6...10) - assert_nil_round_trip(@first_range, :int4_range, 3...3) - end + def test_create_daterange + assert_equal_round_trip(@new_range, :date_range, + Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true)) + end - def test_create_int8range - assert_equal_round_trip(@new_range, :int8_range, Range.new(30, 50, true)) - end + def test_update_daterange + assert_equal_round_trip(@first_range, :date_range, + Date.new(2012, 2, 3)...Date.new(2012, 2, 10)) + assert_nil_round_trip(@first_range, :date_range, + Date.new(2012, 2, 3)...Date.new(2012, 2, 3)) + end - def test_update_int8range - assert_equal_round_trip(@first_range, :int8_range, 60000...10000000) - assert_nil_round_trip(@first_range, :int8_range, 39999...39999) - end + def test_create_int4range + assert_equal_round_trip(@new_range, :int4_range, Range.new(3, 50, true)) + end - def test_exclude_beginning_for_subtypes_without_succ_method_is_not_supported - assert_raises(ArgumentError) { PostgresqlRange.create!(num_range: "(0.1, 0.2]") } - assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") } - assert_raises(ArgumentError) { PostgresqlRange.create!(int4_range: "(1, 10]") } - assert_raises(ArgumentError) { PostgresqlRange.create!(int8_range: "(10, 100]") } - assert_raises(ArgumentError) { PostgresqlRange.create!(date_range: "(''2012-01-02'', ''2012-01-04'']") } - assert_raises(ArgumentError) { PostgresqlRange.create!(ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'']") } - assert_raises(ArgumentError) { PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']") } - end + def test_update_int4range + assert_equal_round_trip(@first_range, :int4_range, 6...10) + assert_nil_round_trip(@first_range, :int4_range, 3...3) + end - def test_where_by_attribute_with_range - range = 1..100 - record = PostgresqlRange.create!(int4_range: range) - assert_equal record, PostgresqlRange.where(int4_range: range).take - end + def test_create_int8range + assert_equal_round_trip(@new_range, :int8_range, Range.new(30, 50, true)) + end - def test_where_by_attribute_with_range_in_array - range = 1..100 - record = PostgresqlRange.create!(int4_range: range) - assert_equal record, PostgresqlRange.where(int4_range: [range]).take - end + def test_update_int8range + assert_equal_round_trip(@first_range, :int8_range, 60000...10000000) + assert_nil_round_trip(@first_range, :int8_range, 39999...39999) + end - def test_update_all_with_ranges - PostgresqlRange.create! + def test_exclude_beginning_for_subtypes_without_succ_method_is_not_supported + assert_raises(ArgumentError) { PostgresqlRange.create!(num_range: "(0.1, 0.2]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(int4_range: "(1, 10]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(int8_range: "(10, 100]") } + assert_raises(ArgumentError) { PostgresqlRange.create!(date_range: "(''2012-01-02'', ''2012-01-04'']") } + assert_raises(ArgumentError) { PostgresqlRange.create!(ts_range: "(''2010-01-01 14:30'', ''2011-01-01 14:30'']") } + assert_raises(ArgumentError) { PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']") } + end - PostgresqlRange.update_all(int8_range: 1..100) + def test_where_by_attribute_with_range + range = 1..100 + record = PostgresqlRange.create!(int4_range: range) + assert_equal record, PostgresqlRange.where(int4_range: range).take + end - assert_equal 1...101, PostgresqlRange.first.int8_range - end + def test_where_by_attribute_with_range_in_array + range = 1..100 + record = PostgresqlRange.create!(int4_range: range) + assert_equal record, PostgresqlRange.where(int4_range: [range]).take + end - def test_ranges_correctly_escape_input - range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a" - PostgresqlRange.update_all(int8_range: range) + def test_update_all_with_ranges + PostgresqlRange.create! - assert_nothing_raised do - PostgresqlRange.first - end - end + PostgresqlRange.update_all(int8_range: 1..100) - def test_infinity_values - PostgresqlRange.create!(int4_range: 1..Float::INFINITY, - int8_range: -Float::INFINITY..0, - float_range: -Float::INFINITY..Float::INFINITY) + assert_equal 1...101, PostgresqlRange.first.int8_range + end - record = PostgresqlRange.first + def test_ranges_correctly_escape_input + range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a" + PostgresqlRange.update_all(int8_range: range) - assert_equal(1...Float::INFINITY, record.int4_range) - assert_equal(-Float::INFINITY...1, record.int8_range) - assert_equal(-Float::INFINITY...Float::INFINITY, record.float_range) + assert_nothing_raised do + PostgresqlRange.first end + end - private - def assert_equal_round_trip(range, attribute, value) - round_trip(range, attribute, value) - assert_equal value, range.public_send(attribute) - end - - def assert_nil_round_trip(range, attribute, value) - round_trip(range, attribute, value) - assert_nil range.public_send(attribute) - end + def test_infinity_values + PostgresqlRange.create!(int4_range: 1..Float::INFINITY, + int8_range: -Float::INFINITY..0, + float_range: -Float::INFINITY..Float::INFINITY) - def round_trip(range, attribute, value) - range.public_send "#{attribute}=", value - assert range.save - assert range.reload - end + record = PostgresqlRange.first - def insert_range(values) - @connection.execute <<-SQL - INSERT INTO postgresql_ranges ( - id, - date_range, - num_range, - ts_range, - tstz_range, - int4_range, - int8_range, - float_range - ) VALUES ( - #{values[:id]}, - '#{values[:date_range]}', - '#{values[:num_range]}', - '#{values[:ts_range]}', - '#{values[:tstz_range]}', - '#{values[:int4_range]}', - '#{values[:int8_range]}', - '#{values[:float_range]}' - ) - SQL - end + assert_equal(1...Float::INFINITY, record.int4_range) + assert_equal(-Float::INFINITY...1, record.int8_range) + assert_equal(-Float::INFINITY...Float::INFINITY, record.float_range) end + + private + def assert_equal_round_trip(range, attribute, value) + round_trip(range, attribute, value) + assert_equal value, range.public_send(attribute) + end + + def assert_nil_round_trip(range, attribute, value) + round_trip(range, attribute, value) + assert_nil range.public_send(attribute) + end + + def round_trip(range, attribute, value) + range.public_send "#{attribute}=", value + assert range.save + assert range.reload + end + + def insert_range(values) + @connection.execute <<~SQL + INSERT INTO postgresql_ranges ( + id, + date_range, + num_range, + ts_range, + tstz_range, + int4_range, + int8_range, + float_range + ) VALUES ( + #{values[:id]}, + '#{values[:date_range]}', + '#{values[:num_range]}', + '#{values[:ts_range]}', + '#{values[:tstz_range]}', + '#{values[:int4_range]}', + '#{values[:int8_range]}', + '#{values[:float_range]}' + ) + SQL + 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 100d247113..7eccaf4aa2 100644 --- a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb +++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb @@ -27,7 +27,7 @@ class PostgresqlRenameTableTest < ActiveRecord::PostgreSQLTestCase private def num_indices_named(name) - @connection.execute(<<-SQL).values.length + @connection.execute(<<~SQL).values.length SELECT 1 FROM "pg_index" JOIN "pg_class" ON "pg_index"."indexrelid" = "pg_class"."oid" WHERE "pg_class"."relname" = '#{name}' diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index a36d066c80..931a9bd0be 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -146,7 +146,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase 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 + ActiveRecord::Base.connection.execute <<~SQL 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); @@ -507,6 +507,7 @@ class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase @connection = ActiveRecord::Base.connection @connection.create_table "trains" do |t| t.string :name + t.string :position t.text :description end end @@ -530,6 +531,17 @@ class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase assert_match(/opclass: \{ description: :text_pattern_ops \}/, output) end + + def test_opclass_class_parsing_on_non_reserved_and_cannot_be_function_or_type_keyword + @connection.enable_extension("pg_trgm") + @connection.execute "CREATE INDEX trains_position ON trains USING gin(position gin_trgm_ops)" + @connection.execute "CREATE INDEX trains_name_and_position ON trains USING btree(name, position text_pattern_ops)" + + output = dump_table_schema "trains" + + assert_match(/opclass: :gin_trgm_ops/, output) + assert_match(/opclass: \{ position: :text_pattern_ops \}/, output) + end end class SchemaIndexNullsOrderTest < ActiveRecord::PostgreSQLTestCase diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb index 984b2f5ea4..919ff3d158 100644 --- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb +++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb @@ -94,7 +94,6 @@ module ActiveRecord end test "raises LockWaitTimeout when lock wait timeout exceeded" do - skip unless ActiveRecord::Base.connection.postgresql_version >= 90300 assert_raises(ActiveRecord::LockWaitTimeout) do s = Sample.create!(value: 1) latch1 = Concurrent::CountDownLatch.new diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 71d07e2f4c..9912763c1b 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -198,10 +198,10 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase # Create custom PostgreSQL function to generate UUIDs # to test dumping tables which columns have defaults with custom functions - connection.execute <<-SQL - CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid - AS $$ SELECT * FROM #{uuid_function} $$ - LANGUAGE SQL VOLATILE; + connection.execute <<~SQL + CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid + AS $$ SELECT * FROM #{uuid_function} $$ + LANGUAGE SQL VOLATILE; SQL # Create such a table with custom function as default value generator diff --git a/activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb b/activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb new file mode 100644 index 0000000000..93a7dafebd --- /dev/null +++ b/activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/topic" + +module ActiveRecord + module ConnectionAdapters + class SQLite3Adapter + class BindParameterTest < ActiveRecord::SQLite3TestCase + def test_too_many_binds + topics = Topic.where(id: (1..999).to_a << 2**63) + assert_equal Topic.count, topics.count + + topics = Topic.where.not(id: (1..999).to_a << 2**63) + assert_equal 0, topics.count + end + 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 89052019f8..56ceb45040 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -55,11 +55,11 @@ module ActiveRecord owner = Owner.create!(name: "hello".encode("ascii-8bit")) owner.reload select = Owner.columns.map { |c| "typeof(#{c.name})" }.join ", " - result = Owner.connection.exec_query <<-esql + result = Owner.connection.exec_query <<~SQL SELECT #{select} FROM #{Owner.table_name} WHERE #{Owner.primary_key} = #{owner.id} - esql + SQL assert_not(result.rows.first.include?("blob"), "should not store blobs") ensure @@ -160,13 +160,13 @@ module ActiveRecord end def test_quote_binary_column_escapes_it - DualEncoding.connection.execute(<<-eosql) + DualEncoding.connection.execute(<<~SQL) CREATE TABLE IF NOT EXISTS dual_encodings ( id integer PRIMARY KEY AUTOINCREMENT, name varchar(255), data binary ) - eosql + SQL str = (+"\x80").force_encoding("ASCII-8BIT") binary = DualEncoding.new name: "いただきます!", data: str binary.save! @@ -261,7 +261,7 @@ module ActiveRecord end def test_tables_logs_name - sql = <<-SQL + sql = <<~SQL SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND type IN ('table') SQL assert_logged [[sql.squish, "SCHEMA", []]] do @@ -271,7 +271,7 @@ module ActiveRecord def test_table_exists_logs_name with_example_table do - sql = <<-SQL + sql = <<~SQL SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND name = 'ex' AND type IN ('table') SQL assert_logged [[sql.squish, "SCHEMA", []]] do @@ -573,6 +573,62 @@ module ActiveRecord end end + def test_errors_when_an_insert_query_is_called_while_preventing_writes + with_example_table "id int, data string" do + assert_raises(ActiveRecord::ReadOnlyError) do + @conn.while_preventing_writes do + @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')") + end + end + end + end + + def test_errors_when_an_update_query_is_called_while_preventing_writes + with_example_table "id int, data string" do + @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @conn.while_preventing_writes do + @conn.execute("UPDATE ex SET data = '9989' WHERE data = '138853948594'") + end + end + end + end + + def test_errors_when_a_delete_query_is_called_while_preventing_writes + with_example_table "id int, data string" do + @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @conn.while_preventing_writes do + @conn.execute("DELETE FROM ex where data = '138853948594'") + end + end + end + end + + def test_errors_when_a_replace_query_is_called_while_preventing_writes + with_example_table "id int, data string" do + @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + assert_raises(ActiveRecord::ReadOnlyError) do + @conn.while_preventing_writes do + @conn.execute("REPLACE INTO ex (data) VALUES ('249823948')") + end + end + end + end + + def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes + with_example_table "id int, data string" do + @conn.execute("INSERT INTO ex (data) VALUES ('138853948594')") + + @conn.while_preventing_writes do + assert_equal 1, @conn.execute("SELECT data from ex WHERE data = '138853948594'").count + end + end + end + private def assert_logged(logs) @@ -585,7 +641,7 @@ module ActiveRecord end def with_example_table(definition = nil, table_name = "ex", &block) - definition ||= <<-SQL + definition ||= <<~SQL id integer PRIMARY KEY AUTOINCREMENT, number integer SQL diff --git a/activerecord/test/cases/arel/visitors/ibm_db_test.rb b/activerecord/test/cases/arel/visitors/ibm_db_test.rb index 7163cb34d3..2ddbec3266 100644 --- a/activerecord/test/cases/arel/visitors/ibm_db_test.rb +++ b/activerecord/test/cases/arel/visitors/ibm_db_test.rb @@ -29,6 +29,45 @@ module Arel sql = compile(stmt) sql.must_be_like "UPDATE \"users\" WHERE \"users\".\"id\" IN (SELECT \"users\".\"id\" FROM \"users\" FETCH FIRST 1 ROWS ONLY)" end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0 + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 0 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 1 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/informix_test.rb b/activerecord/test/cases/arel/visitors/informix_test.rb index b0b031cca3..b6c2dd6ae7 100644 --- a/activerecord/test/cases/arel/visitors/informix_test.rb +++ b/activerecord/test/cases/arel/visitors/informix_test.rb @@ -54,6 +54,45 @@ module Arel sql = compile(stmt) sql.must_be_like 'SELECT FROM "posts" INNER JOIN "comments"' end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + CASE WHEN "users"."name" = 'Aaron Patterson' OR ("users"."name" IS NULL AND 'Aaron Patterson' IS NULL) THEN 0 ELSE 1 END = 0 + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 0 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 1 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/mssql_test.rb b/activerecord/test/cases/arel/visitors/mssql_test.rb index 340376c3d6..74f34b4dad 100644 --- a/activerecord/test/cases/arel/visitors/mssql_test.rb +++ b/activerecord/test/cases/arel/visitors/mssql_test.rb @@ -94,6 +94,45 @@ module Arel sql = compile(stmt) sql.must_be_like "SELECT COUNT(1) as count_id FROM (SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY ) as _row_num) as _t WHERE _row_num BETWEEN 1 AND 10) AS subquery" end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + EXISTS (VALUES ("users"."name") INTERSECT VALUES ('Aaron Patterson')) + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + EXISTS (VALUES ("users"."first_name") INTERSECT VALUES ("users"."last_name")) + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + NOT EXISTS (VALUES ("users"."first_name") INTERSECT VALUES ("users"."last_name")) + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/mysql_test.rb b/activerecord/test/cases/arel/visitors/mysql_test.rb index 9d3bad8516..5f37587957 100644 --- a/activerecord/test/cases/arel/visitors/mysql_test.rb +++ b/activerecord/test/cases/arel/visitors/mysql_test.rb @@ -13,16 +13,6 @@ module Arel @visitor.accept(node, Collectors::SQLString.new).value end - it "squashes parenthesis on multiple unions" do - subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right") - node = Nodes::Union.new subnode, Arel.sql("topright") - assert_equal 1, compile(node).scan("(").length - - subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right") - node = Nodes::Union.new Arel.sql("topleft"), subnode - assert_equal 1, compile(node).scan("(").length - end - ### # :'( # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214 @@ -75,6 +65,45 @@ module Arel } end end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + "users"."name" <=> 'Aaron Patterson' + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + "users"."first_name" <=> "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" <=> NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + NOT "users"."first_name" <=> "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ NOT "users"."name" <=> NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/oracle12_test.rb b/activerecord/test/cases/arel/visitors/oracle12_test.rb index 83a2ee36ca..4ce5cab4db 100644 --- a/activerecord/test/cases/arel/visitors/oracle12_test.rb +++ b/activerecord/test/cases/arel/visitors/oracle12_test.rb @@ -56,6 +56,45 @@ module Arel } end end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0 + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 0 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 1 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/oracle_test.rb b/activerecord/test/cases/arel/visitors/oracle_test.rb index e1dfe40cf9..893edc7f74 100644 --- a/activerecord/test/cases/arel/visitors/oracle_test.rb +++ b/activerecord/test/cases/arel/visitors/oracle_test.rb @@ -192,6 +192,45 @@ module Arel } end end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + DECODE("users"."name", 'Aaron Patterson', 0, 1) = 0 + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 0 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + DECODE("users"."first_name", "users"."last_name", 0, 1) = 1 + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/postgres_test.rb b/activerecord/test/cases/arel/visitors/postgres_test.rb index f7f2c76b6f..0f9efb20b4 100644 --- a/activerecord/test/cases/arel/visitors/postgres_test.rb +++ b/activerecord/test/cases/arel/visitors/postgres_test.rb @@ -276,6 +276,45 @@ module Arel } end end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + "users"."name" IS NOT DISTINCT FROM 'Aaron Patterson' + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + "users"."first_name" IS NOT DISTINCT FROM "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT DISTINCT FROM NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + "users"."first_name" IS DISTINCT FROM "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS DISTINCT FROM NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/sqlite_test.rb b/activerecord/test/cases/arel/visitors/sqlite_test.rb index 6650b6ff3a..ee4e07a675 100644 --- a/activerecord/test/cases/arel/visitors/sqlite_test.rb +++ b/activerecord/test/cases/arel/visitors/sqlite_test.rb @@ -6,7 +6,11 @@ module Arel module Visitors class SqliteTest < Arel::Spec before do - @visitor = SQLite.new Table.engine.connection_pool + @visitor = SQLite.new Table.engine.connection + end + + def compile(node) + @visitor.accept(node, Collectors::SQLString.new).value end it "defaults limit to -1" do @@ -27,6 +31,45 @@ module Arel node = Nodes::False.new() assert_equal "0", @visitor.accept(node, Collectors::SQLString.new).value end + + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + "users"."name" IS 'Aaron Patterson' + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + "users"."first_name" IS "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + "users"."first_name" IS NOT "users"."last_name" + } + end + + it "should handle nil" do + @table = Table.new(:users) + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end end end end diff --git a/activerecord/test/cases/arel/visitors/to_sql_test.rb b/activerecord/test/cases/arel/visitors/to_sql_test.rb index b6426a211e..4bfa799a96 100644 --- a/activerecord/test/cases/arel/visitors/to_sql_test.rb +++ b/activerecord/test/cases/arel/visitors/to_sql_test.rb @@ -155,6 +155,43 @@ module Arel end end + describe "Nodes::IsNotDistinctFrom" do + it "should construct a valid generic SQL statement" do + test = Table.new(:users)[:name].is_not_distinct_from "Aaron Patterson" + compile(test).must_be_like %{ + CASE WHEN "users"."name" = 'Aaron Patterson' OR ("users"."name" IS NULL AND 'Aaron Patterson' IS NULL) THEN 0 ELSE 1 END = 0 + } + end + + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_not_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 0 + } + end + + it "should handle nil" do + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsNotDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NULL } + end + end + + describe "Nodes::IsDistinctFrom" do + it "should handle column names on both sides" do + test = Table.new(:users)[:first_name].is_distinct_from Table.new(:users)[:last_name] + compile(test).must_be_like %{ + CASE WHEN "users"."first_name" = "users"."last_name" OR ("users"."first_name" IS NULL AND "users"."last_name" IS NULL) THEN 0 ELSE 1 END = 1 + } + end + + it "should handle nil" do + val = Nodes.build_quoted(nil, @table[:active]) + sql = compile Nodes::IsDistinctFrom.new(@table[:name], val) + sql.must_be_like %{ "users"."name" IS NOT NULL } + end + end + it "should visit string subclass" do [ Class.new(String).new(":'("), @@ -480,6 +517,28 @@ module Arel end end + describe "Nodes::Union" do + it "squashes parenthesis on multiple unions" do + subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right") + node = Nodes::Union.new subnode, Arel.sql("topright") + assert_equal("( left UNION right UNION topright )", compile(node)) + subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right") + node = Nodes::Union.new Arel.sql("topleft"), subnode + assert_equal("( topleft UNION left UNION right )", compile(node)) + end + end + + describe "Nodes::UnionAll" do + it "squashes parenthesis on multiple union alls" do + subnode = Nodes::UnionAll.new Arel.sql("left"), Arel.sql("right") + node = Nodes::UnionAll.new subnode, Arel.sql("topright") + assert_equal("( left UNION ALL right UNION ALL topright )", compile(node)) + subnode = Nodes::UnionAll.new Arel.sql("left"), Arel.sql("right") + node = Nodes::UnionAll.new Arel.sql("topleft"), subnode + assert_equal("( topleft UNION ALL left UNION ALL right )", compile(node)) + end + end + describe "Nodes::NotIn" do it "should know how to visit" do node = @attr.not_in [1, 2, 3] diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 93dd427951..d1e4ebe86b 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -32,9 +32,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase :posts, :tags, :taggings, :comments, :sponsors, :members def test_belongs_to - firm = Client.find(3).firm - assert_not_nil firm - assert_equal companies(:first_firm).name, firm.name + client = Client.find(3) + first_firm = companies(:first_firm) + assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do + assert_equal first_firm, client.firm + assert_equal first_firm.name, client.firm.name + end end def test_assigning_belongs_to_on_destroyed_object diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 39034746c9..b37e59038e 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -34,7 +34,7 @@ class EagerLoadingTooManyIdsTest < ActiveRecord::TestCase fixtures :citations def test_preloading_too_many_ids - assert_equal Citation.count, Citation.preload(:citations).to_a.size + assert_equal Citation.count, Citation.preload(:reference_of).to_a.size end def test_eager_loading_too_may_ids @@ -1618,32 +1618,6 @@ class EagerAssociationTest < ActiveRecord::TestCase end end - # Associations::Preloader#preloaders_on works with hash-like objects - test "preloading works with an object that responds to :to_hash" do - CustomHash = Class.new(Hash) - - assert_nothing_raised do - Post.preload(CustomHash.new(comments: [{ author: :essays }])).first - end - end - - # Associations::Preloader#preloaders_on works with string-like objects - test "preloading works with an object that responds to :to_str" do - CustomString = Class.new(String) - - assert_nothing_raised do - Post.preload(CustomString.new("comments")).first - end - end - - # Associations::Preloader#preloaders_on does not work with ranges - test "preloading fails when Range is passed" do - exception = assert_raises(ArgumentError) do - Post.preload(1..10).first - end - assert_equal("1..10 was not recognized for preload", exception.message) - end - private def find_all_ordered(klass, include = nil) klass.order("#{klass.table_name}.#{klass.primary_key}").includes(include).to_a 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 38b121d37b..515eb65d37 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 @@ -25,6 +25,8 @@ require "models/user" require "models/member" require "models/membership" require "models/sponsor" +require "models/lesson" +require "models/student" require "models/country" require "models/treaty" require "models/vertex" @@ -275,7 +277,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_habtm_saving_multiple_relationships new_project = Project.new("name" => "Grimetime") amount_of_developers = 4 - developers = (0...amount_of_developers).collect { |i| Developer.create(name: "JME #{i}") }.reverse + developers = (0...amount_of_developers).reverse_each.map { |i| Developer.create(name: "JME #{i}") } new_project.developer_ids = [developers[0].id, developers[1].id] new_project.developers_with_callback_ids = [developers[2].id, developers[3].id] @@ -780,6 +782,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal [projects(:active_record), projects(:action_controller)].map(&:id).sort, developer.project_ids.sort end + def test_singular_ids_are_reloaded_after_collection_concat + student = Student.create(name: "Alberto Almagro") + student.lesson_ids + + lesson = Lesson.create(name: "DSI") + student.lessons << lesson + + assert_includes student.lesson_ids, lesson.id + end + def test_scoped_find_on_through_association_doesnt_return_read_only_records tag = Post.find(1).tags.find_by_name("General") diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index d13e1a86e9..5921193374 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -264,7 +264,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase car = Car.create(name: "honda") car.funky_bulbs.create! assert_equal 1, car.funky_bulbs.count - assert_nothing_raised { car.reload.funky_bulbs.delete_all } + assert_equal 1, car.reload.funky_bulbs.delete_all assert_equal 0, car.funky_bulbs.count, "bulbs should have been deleted using :delete_all strategy" end @@ -294,6 +294,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal(expected_sql, loaded_sql) end + def test_delete_all_on_association_clears_scope + author = Author.create!(name: "Gannon") + posts = author.posts + posts.create!(title: "test", body: "body") + posts.delete_all + assert_nil posts.first + end + def test_building_the_associated_object_with_implicit_sti_base_class firm = DependentFirm.new company = firm.companies.build @@ -1405,7 +1413,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 3, clients.count assert_difference "Client.count", -(clients.count) do - companies(:first_firm).dependent_clients_of_firm.delete_all + assert_equal clients.count, companies(:first_firm).dependent_clients_of_firm.delete_all end end @@ -1502,10 +1510,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_delete_all_with_option_delete_all firm = companies(:first_firm) client_id = firm.dependent_clients_of_firm.first.id - firm.dependent_clients_of_firm.delete_all(:delete_all) + count = firm.dependent_clients_of_firm.count + assert_equal count, firm.dependent_clients_of_firm.delete_all(:delete_all) assert_nil Client.find_by_id(client_id) end + def test_delete_all_with_option_nullify + firm = companies(:first_firm) + client_id = firm.dependent_clients_of_firm.first.id + count = firm.dependent_clients_of_firm.count + assert_equal firm, Client.find(client_id).firm + assert_equal count, firm.dependent_clients_of_firm.delete_all(:nullify) + assert_nil Client.find(client_id).firm + end + def test_delete_all_accepts_limited_parameters firm = companies(:first_firm) assert_raise(ArgumentError) do @@ -1710,6 +1728,30 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert companies(:first_firm).clients_of_firm.reload.empty?, "37signals has no clients after destroy all and refresh" end + def test_destroy_all_on_association_clears_scope + author = Author.create!(name: "Gannon") + posts = author.posts + posts.create!(title: "test", body: "body") + posts.destroy_all + assert_nil posts.first + end + + def test_destroy_on_association_clears_scope + author = Author.create!(name: "Gannon") + posts = author.posts + post = posts.create!(title: "test", body: "body") + posts.destroy(post) + assert_nil posts.first + end + + def test_delete_on_association_clears_scope + author = Author.create!(name: "Gannon") + posts = author.posts + post = posts.create!(title: "test", body: "body") + posts.delete(post) + assert_nil posts.first + end + def test_dependence firm = companies(:first_firm) assert_equal 3, firm.clients.size @@ -2002,8 +2044,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_respects_hash_conditions - assert_equal authors(:david).hello_posts, authors(:david).hello_posts_with_hash_conditions - assert_equal authors(:david).hello_post_comments, authors(:david).hello_post_comments_with_hash_conditions + assert_equal authors(:david).hello_posts.sort_by(&:id), authors(:david).hello_posts_with_hash_conditions.sort_by(&:id) + assert_equal authors(:david).hello_post_comments.sort_by(&:id), authors(:david).hello_post_comments_with_hash_conditions.sort_by(&:id) end def test_include_uses_array_include_after_loaded 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 7b405c74c4..77c3ed954a 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -200,7 +200,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_no_difference "Job.count" do assert_difference "Reference.count", -1 do - person.reload.jobs_with_dependent_destroy.delete_all + assert_equal 1, person.reload.jobs_with_dependent_destroy.delete_all end end end @@ -211,7 +211,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_no_difference "Job.count" do assert_no_difference "Reference.count" do - person.reload.jobs_with_dependent_nullify.delete_all + assert_equal 1, person.reload.jobs_with_dependent_nullify.delete_all end end end @@ -222,11 +222,19 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_no_difference "Job.count" do assert_difference "Reference.count", -1 do - person.reload.jobs_with_dependent_delete_all.delete_all + assert_equal 1, person.reload.jobs_with_dependent_delete_all.delete_all end end end + def test_delete_all_on_association_clears_scope + post = Post.create!(title: "Rails 6", body: "") + people = post.people + people.create!(first_name: "Jeb") + people.delete_all + assert_nil people.first + end + def test_concat person = people(:david) post = posts(:thinking) @@ -401,6 +409,30 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_empty posts(:welcome).people.reload end + def test_destroy_all_on_association_clears_scope + post = Post.create!(title: "Rails 6", body: "") + people = post.people + people.create!(first_name: "Jeb") + people.destroy_all + assert_nil people.first + end + + def test_destroy_on_association_clears_scope + post = Post.create!(title: "Rails 6", body: "") + people = post.people + person = people.create!(first_name: "Jeb") + people.destroy(person) + assert_nil people.first + end + + def test_delete_on_association_clears_scope + post = Post.create!(title: "Rails 6", body: "") + people = post.people + person = people.create!(first_name: "Jeb") + people.delete(person) + assert_nil people.first + end + def test_should_raise_exception_for_destroying_mismatching_records assert_no_difference ["Person.count", "Reader.count"] do assert_raise(ActiveRecord::AssociationTypeMismatch) { posts(:welcome).people.destroy(posts(:thinking)) } @@ -586,6 +618,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_not_includes posts(:welcome).reload.people.reload, people(:michael) end + def test_replace_association_with_duplicates + post = posts(:thinking) + person = people(:david) + + assert_difference "post.people.count", 2 do + post.people = [person] + post.people = [person, person] + end + end + def test_replace_order_is_preserved posts(:welcome).people.clear posts(:welcome).people = [people(:david), people(:michael)] diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index adfb3ce072..bf574f6637 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -22,8 +22,12 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end def test_has_one - assert_equal companies(:first_firm).account, Account.find(1) - assert_equal Account.find(1).credit_limit, companies(:first_firm).account.credit_limit + firm = companies(:first_firm) + first_account = Account.find(1) + assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do + assert_equal first_account, firm.account + assert_equal first_account.credit_limit, firm.account.credit_limit + end end def test_has_one_does_not_use_order_by 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 0309663943..69b4872519 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -35,6 +35,13 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase assert_equal clubs(:boring_club), @member.club end + def test_has_one_through_executes_limited_query + boring_club = clubs(:boring_club) + assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do + assert_equal boring_club, @member.general_club + end + end + def test_creating_association_creates_through_record new_member = Member.create(name: "Chris") new_member.club = Club.create(name: "LRUG") diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 0bfd46a522..d341dd0083 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -56,6 +56,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]", t.attribute_for_inspect(:content) end + test "attribute_for_inspect with a non-primary key id attribute" do + t = topics(:first).becomes(TitlePrimaryKeyTopic) + t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters" + + assert_equal "1", t.attribute_for_inspect(:id) + end + test "attribute_present" do t = Topic.new t.title = "hello there!" @@ -310,6 +317,18 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "New topic", topic.title end + test "write_attribute raises ActiveModel::MissingAttributeError when the attribute does not exist" do + topic = Topic.first + assert_raises(ActiveModel::MissingAttributeError) { topic.update_columns(no_column_exists: "Hello!") } + assert_raises(ActiveModel::UnknownAttributeError) { topic.update(no_column_exists: "Hello!") } + end + + test "write_attribute allows writing to aliased attributes" do + topic = Topic.first + assert_nothing_raised { topic.update_columns(heading: "Hello!") } + assert_nothing_raised { topic.update(heading: "Hello!") } + end + test "read_attribute" do topic = Topic.new topic.title = "Don't change the topic" @@ -436,7 +455,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase end test "custom field attribute predicate" do - object = Company.find_by_sql(<<-SQL).first + object = Company.find_by_sql(<<~SQL).first SELECT c1.*, c2.type as string_value, c2.rating as int_value FROM companies c1, companies c2 WHERE c1.firm_id = c2.id diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 09e5517449..b74bd93f37 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -1447,6 +1447,14 @@ class BasicsTest < ActiveRecord::TestCase assert_not_respond_to developer, :first_name= end + test "when ignored attribute is loaded, cast type should be preferred over DB type" do + developer = AttributedDeveloper.create + developer.update_column :name, "name" + + loaded_developer = AttributedDeveloper.where(id: developer.id).select("*").first + assert_equal "Developer: name", loaded_developer.name + end + test "ignored columns not included in SELECT" do query = Developer.all.to_sql.downcase @@ -1480,4 +1488,40 @@ class BasicsTest < ActiveRecord::TestCase ensure ActiveRecord::Base.protected_environments = previous_protected_environments end + + test "creating a record raises if preventing writes" do + assert_raises ActiveRecord::ReadOnlyError do + ActiveRecord::Base.connection.while_preventing_writes do + Bird.create! name: "Bluejay" + end + end + end + + test "updating a record raises if preventing writes" do + bird = Bird.create! name: "Bluejay" + + assert_raises ActiveRecord::ReadOnlyError do + ActiveRecord::Base.connection.while_preventing_writes do + bird.update! name: "Robin" + end + end + end + + test "deleting a record raises if preventing writes" do + bird = Bird.create! name: "Bluejay" + + assert_raises ActiveRecord::ReadOnlyError do + ActiveRecord::Base.connection.while_preventing_writes do + bird.destroy! + end + end + end + + test "selecting a record does not raise if preventing writes" do + bird = Bird.create! name: "Bluejay" + + ActiveRecord::Base.connection.while_preventing_writes do + assert_equal bird, Bird.where(name: "Bluejay").first + end + end end diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 9c1f7aaef2..bd5f157ca1 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -36,8 +36,12 @@ if ActiveRecord::Base.connection.prepared_statements def test_too_many_binds bind_params_length = @connection.send(:bind_params_length) - topics = Topic.where(id: (1 .. bind_params_length + 1).to_a) + + topics = Topic.where(id: (1 .. bind_params_length).to_a << 2**63) assert_equal Topic.count, topics.count + + topics = Topic.where.not(id: (1 .. bind_params_length).to_a << 2**63) + assert_equal 0, topics.count end def test_bind_from_join_in_subquery diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb index 3a569f226e..3a06b1c795 100644 --- a/activerecord/test/cases/cache_key_test.rb +++ b/activerecord/test/cases/cache_key_test.rb @@ -44,10 +44,88 @@ module ActiveRecord test "cache_key_with_version always has both key and version" do r1 = CacheMeWithVersion.create - assert_equal "active_record/cache_key_test/cache_me_with_versions/#{r1.id}-#{r1.updated_at.to_s(:usec)}", r1.cache_key_with_version + assert_equal "active_record/cache_key_test/cache_me_with_versions/#{r1.id}-#{r1.updated_at.utc.to_s(:usec)}", r1.cache_key_with_version r2 = CacheMe.create - assert_equal "active_record/cache_key_test/cache_mes/#{r2.id}-#{r2.updated_at.to_s(:usec)}", r2.cache_key_with_version + assert_equal "active_record/cache_key_test/cache_mes/#{r2.id}-#{r2.updated_at.utc.to_s(:usec)}", r2.cache_key_with_version + end + + test "cache_version is the same when it comes from the DB or from the user" do + skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter) + + record = CacheMeWithVersion.create + record_from_db = CacheMeWithVersion.find(record.id) + assert_not_called(record_from_db, :updated_at) do + record_from_db.cache_version + end + + assert_equal record.cache_version, record_from_db.cache_version + end + + test "cache_version does not truncate zeros when timestamp ends in zeros" do + skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter) + + travel_to Time.now.beginning_of_day do + record = CacheMeWithVersion.create + record_from_db = CacheMeWithVersion.find(record.id) + assert_not_called(record_from_db, :updated_at) do + record_from_db.cache_version + end + + assert_equal record.cache_version, record_from_db.cache_version + end + end + + test "cache_version calls updated_at when the value is generated at create time" do + record = CacheMeWithVersion.create + assert_called(record, :updated_at) do + record.cache_version + end + end + + test "cache_version does NOT call updated_at when value is from the database" do + skip("Mysql2 does not return a string value for updated_at") if current_adapter?(:Mysql2Adapter) + + record = CacheMeWithVersion.create + record_from_db = CacheMeWithVersion.find(record.id) + assert_not_called(record_from_db, :updated_at) do + record_from_db.cache_version + end + end + + test "cache_version does call updated_at when it is assigned via a Time object" do + record = CacheMeWithVersion.create + record_from_db = CacheMeWithVersion.find(record.id) + assert_called(record_from_db, :updated_at) do + record_from_db.updated_at = Time.now + record_from_db.cache_version + end + end + + test "cache_version does call updated_at when it is assigned via a string" do + record = CacheMeWithVersion.create + record_from_db = CacheMeWithVersion.find(record.id) + assert_called(record_from_db, :updated_at) do + record_from_db.updated_at = Time.now.to_s + record_from_db.cache_version + end + end + + test "cache_version does call updated_at when it is assigned via a hash" do + record = CacheMeWithVersion.create + record_from_db = CacheMeWithVersion.find(record.id) + assert_called(record_from_db, :updated_at) do + record_from_db.updated_at = { 1 => 2016, 2 => 11, 3 => 12, 4 => 1, 5 => 2, 6 => 3, 7 => 22 } + record_from_db.cache_version + end + end + + test "updated_at on class but not on instance raises an error" do + record = CacheMeWithVersion.create + record_from_db = CacheMeWithVersion.where(id: record.id).select(:id).first + assert_raises(ActiveModel::MissingAttributeError) do + record_from_db.cache_version + end end end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 5c9ed42173..4d3db912c5 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -57,8 +57,12 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 3, value end - def test_should_return_nil_as_average - assert_nil NumericData.average(:bank_balance) + def test_should_return_nil_to_d_as_average + if nil.respond_to?(:to_d) + assert_equal BigDecimal(0), NumericData.average(:bank_balance) + else + assert_nil NumericData.average(:bank_balance) + end end def test_should_get_maximum_of_field @@ -218,8 +222,8 @@ class CalculationsTest < ActiveRecord::TestCase Account.select("credit_limit, firm_name").count } - assert_match %r{accounts}i, e.message - assert_match "credit_limit, firm_name", e.message + assert_match %r{accounts}i, e.sql + assert_match "credit_limit, firm_name", e.sql end def test_apply_distinct_in_count @@ -717,6 +721,10 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal [], Topic.includes(:replies).order(:id).offset(5).pluck(:id) end + def test_pluck_with_join + assert_equal [[2, 2], [4, 4]], Reply.includes(:topic).pluck(:id, :"topics.id") + end + def test_group_by_with_limit expected = { "Post" => 8, "SpecialPost" => 1 } actual = Post.includes(:comments).group(:type).order(:type).limit(2).count("comments.id") diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb index 844b2b2162..483383257b 100644 --- a/activerecord/test/cases/collection_cache_key_test.rb +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -42,6 +42,20 @@ module ActiveRecord assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 end + test "cache_key for relation with custom select and limit" do + developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5) + developers_with_select = developers.select("developers.*") + last_developer_timestamp = developers.first.updated_at + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers_with_select.cache_key) + + /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers_with_select.cache_key + + assert_equal ActiveSupport::Digest.hexdigest(developers_with_select.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 "cache_key for loaded relation" do developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5).load last_developer_timestamp = developers.first.updated_at diff --git a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb index d4e8cbee81..393b577c1d 100644 --- a/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb @@ -120,6 +120,77 @@ module ActiveRecord ENV["RAILS_ENV"] = previous_env end + def test_switching_connections_with_database_url + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + previous_url, ENV["DATABASE_URL"] = ENV["DATABASE_URL"], "postgres://localhost/foo" + + ActiveRecord::Base.connected_to(database: { writing: "postgres://localhost/bar" }) do + handler = ActiveRecord::Base.connection_handler + assert_equal handler, ActiveRecord::Base.connection_handlers[:writing] + + assert_not_nil pool = handler.retrieve_connection_pool("primary") + assert_equal({ adapter: "postgresql", database: "bar", host: "localhost" }, pool.spec.config) + end + ensure + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + ENV["DATABASE_URL"] = previous_url + end + + def test_switching_connections_with_database_config_hash + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + config = { adapter: "sqlite3", database: "db/readonly.sqlite3" } + + ActiveRecord::Base.connected_to(database: { writing: config }) do + handler = ActiveRecord::Base.connection_handler + assert_equal handler, ActiveRecord::Base.connection_handlers[:writing] + + assert_not_nil pool = handler.retrieve_connection_pool("primary") + assert_equal(config, pool.spec.config) + end + ensure + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + end + + def test_switching_connections_with_database_and_role_raises + error = assert_raises(ArgumentError) do + ActiveRecord::Base.connected_to(database: :readonly, role: :writing) { } + end + assert_equal "connected_to can only accept a `database` or a `role` argument, but not both arguments.", error.message + end + + def test_switching_connections_without_database_and_role_raises + error = assert_raises(ArgumentError) do + ActiveRecord::Base.connected_to { } + end + assert_equal "must provide a `database` or a `role`.", error.message + end + + def test_switching_connections_with_database_symbol + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "readonly" => { adapter: "sqlite3", database: "db/readonly.sqlite3" }, + "primary" => { adapter: "sqlite3", database: "db/primary.sqlite3" } + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.connected_to(database: :readonly) do + handler = ActiveRecord::Base.connection_handler + assert_equal handler, ActiveRecord::Base.connection_handlers[:readonly] + + assert_not_nil pool = handler.retrieve_connection_pool("primary") + assert_equal(config["default_env"]["readonly"], pool.spec.config) + end + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + ENV["RAILS_ENV"] = previous_env + end + def test_connects_to_with_single_configuration config = { "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, @@ -150,6 +221,27 @@ module ActiveRecord ActiveRecord::Base.configurations = @prev_configs ActiveRecord::Base.establish_connection(:arunit) end + + def test_connects_to_returns_array_of_established_connections + config = { + "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, + "development_readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + result = ActiveRecord::Base.connects_to database: { writing: :development, reading: :development_readonly } + + assert_equal( + [ + ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("primary"), + ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary") + ], + result + ) + ensure + ActiveRecord::Base.configurations = @prev_configs + ActiveRecord::Base.establish_connection(:arunit) + end end def test_connection_pools @@ -188,6 +280,43 @@ module ActiveRecord assert_nil @rw_handler.retrieve_connection_pool("foo") assert_nil @ro_handler.retrieve_connection_pool("foo") end + + def test_connection_handlers_are_per_thread_and_not_per_fiber + original_handlers = ActiveRecord::Base.connection_handlers + + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler, reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new } + + reading_handler = ActiveRecord::Base.connection_handlers[:reading] + + reading = ActiveRecord::Base.with_handler(:reading) do + Person.connection_handler + end + + assert_not_equal reading, ActiveRecord::Base.connection_handler + assert_equal reading, reading_handler + ensure + ActiveRecord::Base.connection_handlers = original_handlers + end + + def test_connection_handlers_swapping_connections_in_fiber + original_handlers = ActiveRecord::Base.connection_handlers + + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler, reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new } + + reading_handler = ActiveRecord::Base.connection_handlers[:reading] + + enum = Enumerator.new do |r| + r << ActiveRecord::Base.connection_handler + end + + reading = ActiveRecord::Base.with_handler(:reading) do + enum.next + end + + assert_equal reading, reading_handler + ensure + ActiveRecord::Base.connection_handlers = original_handlers + end end end end diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb index f7fbf3ee8a..36e3d543cd 100644 --- a/activerecord/test/cases/core_test.rb +++ b/activerecord/test/cases/core_test.rb @@ -30,6 +30,11 @@ class CoreTest < ActiveRecord::TestCase assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.all.merge!(select: "id, title", where: "id = 1").first.inspect end + def test_inspect_instance_with_non_primary_key_id_attribute + topic = topics(:first).becomes(TitlePrimaryKeyTopic) + assert_match(/id: 1/, topic.inspect) + end + def test_inspect_class_without_table assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect end @@ -110,4 +115,11 @@ class CoreTest < ActiveRecord::TestCase PP.pp(subtopic.new, StringIO.new(actual)) assert_equal "inspecting topic\n", actual end + + def test_pretty_print_with_non_primary_key_id_attribute + topic = topics(:first).becomes(TitlePrimaryKeyTopic) + actual = +"" + PP.pp(topic, StringIO.new(actual)) + assert_match(/id: 1/, actual) + end end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 0f957d41cf..5d02e59ef6 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -106,6 +106,13 @@ if current_adapter?(:Mysql2Adapter) class MysqlDefaultExpressionTest < ActiveRecord::TestCase include SchemaDumpingHelper + if supports_default_expression? + test "schema dump includes default expression" do + output = dump_table_schema("defaults") + assert_match %r/t\.binary\s+"uuid",\s+limit: 36,\s+default: -> { "\(uuid\(\)\)" }/i, output + end + end + if subsecond_precision_supported? test "schema dump datetime includes default expression" do output = dump_table_schema("datetime_defaults") diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index b4593ccdf2..8a0f6f6df1 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -274,6 +274,24 @@ class EnumTest < ActiveRecord::TestCase end assert_match(/must be either a hash, an array of symbols, or an array of strings./, e.message) + + e = assert_raises(ArgumentError) do + Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: { "" => 1, "active" => 2 } + end + end + + assert_match(/Enum label name must not be blank/, e.message) + + e = assert_raises(ArgumentError) do + Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: ["active", ""] + end + end + + assert_match(/Enum label name must not be blank/, e.message) end test "reserved enum names" do @@ -420,6 +438,20 @@ class EnumTest < ActiveRecord::TestCase assert_equal ["drafted", "uploaded"], book2.status_change end + test "attempting to modify enum raises error" do + e = assert_raises(RuntimeError) do + Book.statuses["bad_enum"] = 40 + end + + assert_match(/can't modify frozen/, e.message) + + e = assert_raises(RuntimeError) do + Book.statuses.delete("published") + end + + assert_match(/can't modify frozen/, e.message) + end + test "declare multiple enums at a time" do klass = Class.new(ActiveRecord::Base) do self.table_name = "books" @@ -519,4 +551,13 @@ class EnumTest < ActiveRecord::TestCase test "data type of Enum type" do assert_equal :integer, Book.type_for_attribute("status").type end + + test "scopes can be disabled" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: [:proposed, :written], _scopes: false + end + + assert_raises(NoMethodError) { klass.proposed } + end end diff --git a/activerecord/test/cases/filter_attributes_test.rb b/activerecord/test/cases/filter_attributes_test.rb index af5badd87d..47161a547a 100644 --- a/activerecord/test/cases/filter_attributes_test.rb +++ b/activerecord/test/cases/filter_attributes_test.rb @@ -4,6 +4,7 @@ require "cases/helper" require "models/admin" require "models/admin/user" require "models/admin/account" +require "models/user" require "pp" class FilterAttributesTest < ActiveRecord::TestCase @@ -30,6 +31,32 @@ class FilterAttributesTest < ActiveRecord::TestCase end end + test "string filter_attributes perform pertial match" do + ActiveRecord::Base.filter_attributes = ["n"] + Admin::Account.all.each do |account| + assert_includes account.inspect, "name: [FILTERED]" + assert_equal 1, account.inspect.scan("[FILTERED]").length + end + end + + test "regex filter_attributes are accepted" do + ActiveRecord::Base.filter_attributes = [/\An\z/] + account = Admin::Account.find_by(name: "37signals") + assert_includes account.inspect, 'name: "37signals"' + assert_equal 0, account.inspect.scan("[FILTERED]").length + + ActiveRecord::Base.filter_attributes = [/\An/] + account = Admin::Account.find_by(name: "37signals") + assert_includes account.reload.inspect, "name: [FILTERED]" + assert_equal 1, account.inspect.scan("[FILTERED]").length + end + + test "proc filter_attributes are accepted" do + ActiveRecord::Base.filter_attributes = [ lambda { |key, value| value.reverse! if key == "name" } ] + account = Admin::Account.find_by(name: "37signals") + assert_includes account.inspect, 'name: "slangis73"' + end + test "filter_attributes could be overwritten by models" do Admin::Account.all.each do |account| assert_includes account.inspect, "name: [FILTERED]" @@ -37,7 +64,6 @@ class FilterAttributesTest < ActiveRecord::TestCase end begin - previous_account_filter_attributes = Admin::Account.filter_attributes Admin::Account.filter_attributes = [] # Above changes should not impact other models @@ -51,7 +77,7 @@ class FilterAttributesTest < ActiveRecord::TestCase assert_equal 0, account.inspect.scan("[FILTERED]").length end ensure - Admin::Account.filter_attributes = previous_account_filter_attributes + Admin::Account.remove_instance_variable(:@filter_attributes) end end @@ -63,6 +89,18 @@ class FilterAttributesTest < ActiveRecord::TestCase assert_equal 0, account.inspect.scan("[FILTERED]").length end + test "filter_attributes should handle [FILTERED] value properly" do + begin + User.filter_attributes = ["auth"] + user = User.new(token: "[FILTERED]", auth_token: "[FILTERED]") + + assert_includes user.inspect, "auth_token: [FILTERED]" + assert_includes user.inspect, 'token: "[FILTERED]"' + ensure + User.remove_instance_variable(:@filter_attributes) + end + end + test "filter_attributes on pretty_print" do user = admin_users(:david) actual = "".dup @@ -81,4 +119,18 @@ class FilterAttributesTest < ActiveRecord::TestCase assert_not_includes actual, "name: [FILTERED]" assert_equal 0, actual.scan("[FILTERED]").length end + + test "filter_attributes on pretty_print should handle [FILTERED] value properly" do + begin + User.filter_attributes = ["auth"] + user = User.new(token: "[FILTERED]", auth_token: "[FILTERED]") + actual = "".dup + PP.pp(user, StringIO.new(actual)) + + assert_includes actual, "auth_token: [FILTERED]" + assert_includes actual, 'token: "[FILTERED]"' + ensure + User.remove_instance_variable(:@filter_attributes) + end + end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 355fb4517f..21e84d850b 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -20,6 +20,7 @@ require "models/matey" require "models/dog" require "models/car" require "models/tyre" +require "models/subscriber" class FinderTest < ActiveRecord::TestCase fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :author_addresses, :customers, :categories, :categorizations, :cars @@ -167,6 +168,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Topic.exists?(id: [1, 9999]) assert_equal false, Topic.exists?(45) + assert_equal false, Topic.exists?(9999999999999999999999999999999) assert_equal false, Topic.exists?(Topic.new.id) assert_raise(NoMethodError) { Topic.exists?([1, 2]) } @@ -211,17 +213,23 @@ class FinderTest < ActiveRecord::TestCase assert_equal false, relation.exists?(false) end + def test_exists_with_string + assert_equal false, Subscriber.exists?("foo") + assert_equal false, Subscriber.exists?(" ") + + Subscriber.create!(id: "foo") + Subscriber.create!(id: " ") + + assert_equal true, Subscriber.exists?("foo") + assert_equal true, Subscriber.exists?(" ") + end + def test_exists_passing_active_record_object_is_not_permitted assert_raises(ArgumentError) do Topic.exists?(Topic.new) end end - def test_exists_returns_false_when_parameter_has_invalid_type - assert_equal false, Topic.exists?("foo") - assert_equal false, Topic.exists?(("9" * 53).to_i) # number that's bigger than int - end - def test_exists_does_not_select_columns_without_alias assert_sql(/SELECT\W+1 AS one FROM ["`]topics["`]/i) do Topic.exists? @@ -246,6 +254,10 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Topic.first.replies.exists? end + def test_exists_with_empty_hash_arg + assert_equal true, Topic.exists?({}) + end + # Ensure +exists?+ runs without an error by excluding distinct value. # See https://github.com/rails/rails/pull/26981. def test_exists_with_order_and_distinct @@ -729,6 +741,16 @@ class FinderTest < ActiveRecord::TestCase assert_equal expected, clients.limit(5).first(2) end + def test_implicit_order_column_is_configurable + old_implicit_order_column = Topic.implicit_order_column + Topic.implicit_order_column = "title" + + assert_equal topics(:fifth), Topic.first + assert_equal topics(:third), Topic.last + ensure + Topic.implicit_order_column = old_implicit_order_column + end + def test_take_and_first_and_last_with_integer_should_return_an_array assert_kind_of Array, Topic.take(5) assert_kind_of Array, Topic.first(5) diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 82ca15b415..a5592fc86a 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -473,11 +473,11 @@ class FixturesTest < ActiveRecord::TestCase end def test_empty_yaml_fixture - assert_not_nil ActiveRecord::FixtureSet.new(Account.connection, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts") + assert_not_nil ActiveRecord::FixtureSet.new(nil, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts") end def test_empty_yaml_fixture_with_a_comment_in_it - assert_not_nil ActiveRecord::FixtureSet.new(Account.connection, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies") + assert_not_nil ActiveRecord::FixtureSet.new(nil, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies") end def test_nonexistent_fixture_file @@ -487,14 +487,14 @@ class FixturesTest < ActiveRecord::TestCase assert_empty Dir[nonexistent_fixture_path + "*"] assert_raise(Errno::ENOENT) do - ActiveRecord::FixtureSet.new(Account.connection, "companies", Company, nonexistent_fixture_path) + ActiveRecord::FixtureSet.new(nil, "companies", Company, nonexistent_fixture_path) end end def test_dirty_dirty_yaml_file fixture_path = FIXTURES_ROOT + "/naked/yml/courses" error = assert_raise(ActiveRecord::Fixture::FormatError) do - ActiveRecord::FixtureSet.new(Account.connection, "courses", Course, fixture_path) + ActiveRecord::FixtureSet.new(nil, "courses", Course, fixture_path) end assert_equal "fixture is not a hash: #{fixture_path}.yml", error.to_s end @@ -502,7 +502,7 @@ class FixturesTest < ActiveRecord::TestCase def test_yaml_file_with_one_invalid_fixture fixture_path = FIXTURES_ROOT + "/naked/yml/courses_with_invalid_key" error = assert_raise(ActiveRecord::Fixture::FormatError) do - ActiveRecord::FixtureSet.new(Account.connection, "courses", Course, fixture_path) + ActiveRecord::FixtureSet.new(nil, "courses", Course, fixture_path) end assert_equal "fixture key is not a hash: #{fixture_path}.yml, keys: [\"two\"]", error.to_s end @@ -525,7 +525,7 @@ class FixturesTest < ActiveRecord::TestCase def test_omap_fixtures assert_nothing_raised do - fixtures = ActiveRecord::FixtureSet.new(Account.connection, "categories", Category, FIXTURES_ROOT + "/categories_ordered") + fixtures = ActiveRecord::FixtureSet.new(nil, "categories", Category, FIXTURES_ROOT + "/categories_ordered") fixtures.each.with_index do |(name, fixture), i| assert_equal "fixture_no_#{i}", name @@ -596,7 +596,7 @@ class HasManyThroughFixture < ActiveRecord::TestCase parrots = File.join FIXTURES_ROOT, "parrots" - fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots + fs = ActiveRecord::FixtureSet.new(nil, "parrots", parrot, parrots) rows = fs.table_rows assert_equal load_has_and_belongs_to_many["parrots_treasures"], rows["parrots_treasures"] end @@ -614,18 +614,22 @@ class HasManyThroughFixture < ActiveRecord::TestCase parrots = File.join FIXTURES_ROOT, "parrots" - fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots + fs = ActiveRecord::FixtureSet.new(nil, "parrots", parrot, parrots) rows = fs.table_rows assert_equal load_has_and_belongs_to_many["parrots_treasures"], rows["parrot_treasures"] end + def test_has_and_belongs_to_many_order + assert_equal ["parrots", "parrots_treasures"], load_has_and_belongs_to_many.keys + end + def load_has_and_belongs_to_many parrot = make_model "Parrot" parrot.has_and_belongs_to_many :treasures parrots = File.join FIXTURES_ROOT, "parrots" - fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots + fs = ActiveRecord::FixtureSet.new(nil, "parrots", parrot, parrots) fs.table_rows end end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 68be685e4b..730cd663a2 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -48,6 +48,15 @@ def mysql_enforcing_gtid_consistency? current_adapter?(:Mysql2Adapter) && "ON" == ActiveRecord::Base.connection.show_variable("enforce_gtid_consistency") end +def supports_default_expression? + if current_adapter?(:PostgreSQLAdapter) + true + elsif current_adapter?(:Mysql2Adapter) + conn = ActiveRecord::Base.connection + !conn.mariadb? && conn.version >= "8.0.13" + end +end + def supports_savepoints? ActiveRecord::Base.connection.supports_savepoints? end diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb index e7778af55b..7b388ebc5e 100644 --- a/activerecord/test/cases/hot_compatibility_test.rb +++ b/activerecord/test/cases/hot_compatibility_test.rb @@ -56,7 +56,7 @@ class HotCompatibilityTest < ActiveRecord::TestCase assert_equal "bar", record.foo end - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter) && ActiveRecord::Base.connection.prepared_statements test "cleans up after prepared statement failure in a transaction" do with_two_connections do |original_connection, ddl_connection| record = @klass.create! bar: "bar" diff --git a/activerecord/test/cases/instrumentation_test.rb b/activerecord/test/cases/instrumentation_test.rb index e6e8468757..c09ea32991 100644 --- a/activerecord/test/cases/instrumentation_test.rb +++ b/activerecord/test/cases/instrumentation_test.rb @@ -5,6 +5,10 @@ require "models/book" module ActiveRecord class InstrumentationTest < ActiveRecord::TestCase + def setup + ActiveRecord::Base.connection.schema_cache.add(Book.table_name) + end + def test_payload_name_on_load Book.create(name: "test book") subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 5d060c8899..661163b4a1 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -127,6 +127,36 @@ class MigrationTest < ActiveRecord::TestCase assert_equal 20131219224947, migrator.current_version end + def test_create_table_raises_if_already_exists + connection = Person.connection + connection.create_table :testings, force: true do |t| + t.string :foo + end + + assert_raise(ActiveRecord::StatementInvalid) do + connection.create_table :testings do |t| + t.string :foo + end + end + ensure + connection.drop_table :testings, if_exists: true + end + + def test_create_table_with_if_not_exists_true + connection = Person.connection + connection.create_table :testings, force: true do |t| + t.string :foo + end + + assert_nothing_raised do + connection.create_table :testings, if_not_exists: true do |t| + t.string :foo + end + end + ensure + connection.drop_table :testings, if_exists: true + end + def test_create_table_with_force_true_does_not_drop_nonexisting_table # using a copy as we need the drop_table method to # continue to work for the ensure block of the test diff --git a/activerecord/test/cases/numeric_data_test.rb b/activerecord/test/cases/numeric_data_test.rb index 14db63890e..079e664ee4 100644 --- a/activerecord/test/cases/numeric_data_test.rb +++ b/activerecord/test/cases/numeric_data_test.rb @@ -24,8 +24,10 @@ class NumericDataTest < ActiveRecord::TestCase ) assert m.save - m1 = NumericData.find(m.id) - assert_not_nil m1 + m1 = NumericData.find_by( + bank_balance: 1586.43, + big_bank_balance: BigDecimal("1000234000567.95") + ) assert_kind_of Integer, m1.world_population assert_equal 2**62, m1.world_population @@ -49,8 +51,10 @@ class NumericDataTest < ActiveRecord::TestCase ) assert m.save - m1 = NumericData.find(m.id) - assert_not_nil m1 + m1 = NumericData.find_by( + bank_balance: 1586.43122334, + big_bank_balance: BigDecimal("234000567.952344") + ) assert_kind_of Integer, m1.world_population assert_equal 2**62, m1.world_population @@ -64,4 +68,26 @@ class NumericDataTest < ActiveRecord::TestCase assert_kind_of BigDecimal, m1.big_bank_balance assert_equal BigDecimal("234000567.95"), m1.big_bank_balance end + + if current_adapter?(:PostgreSQLAdapter) + def test_numeric_fields_with_nan + m = NumericData.new( + bank_balance: BigDecimal("NaN"), + big_bank_balance: BigDecimal("NaN"), + world_population: 2**62, + my_house_population: 3 + ) + assert_predicate m.bank_balance, :nan? + assert_predicate m.big_bank_balance, :nan? + assert m.save + + m1 = NumericData.find_by( + bank_balance: BigDecimal("NaN"), + big_bank_balance: BigDecimal("NaN") + ) + + assert_predicate m1.bank_balance, :nan? + assert_predicate m1.big_bank_balance, :nan? + end + end end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 4ed7469039..4759d3b6b2 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -354,7 +354,6 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase end def test_composite_primary_key_out_of_order - skip if current_adapter?(:SQLite3Adapter) assert_equal ["code", "region"], @connection.primary_keys("barcodes_reverse") end @@ -376,7 +375,6 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase end def test_dumping_composite_primary_key_out_of_order - skip if current_adapter?(:SQLite3Adapter) schema = dump_table_schema "barcodes_reverse" assert_match %r{create_table "barcodes_reverse", primary_key: \["code", "region"\]}, schema end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 565190c476..02ead8d914 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -55,6 +55,22 @@ class QueryCacheTest < ActiveRecord::TestCase assert_cache :off end + def test_query_cache_is_applied_to_connections_in_all_handlers + ActiveRecord::Base.connected_to(role: :reading) do + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"]) + end + + mw = middleware { |env| + ro_conn = ActiveRecord::Base.connection_handlers[:reading].connection_pool_list.first.connection + assert_predicate ActiveRecord::Base.connection, :query_cache_enabled + assert_predicate ro_conn, :query_cache_enabled + } + + mw.call({}) + ensure + ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler } + end + def test_query_cache_across_threads with_temporary_connection_pool do begin diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb index 8703d238a0..0b06cec40b 100644 --- a/activerecord/test/cases/relation/where_clause_test.rb +++ b/activerecord/test/cases/relation/where_clause_test.rb @@ -92,12 +92,16 @@ class ActiveRecord::Relation original = WhereClause.new([ table["id"].in([1, 2, 3]), table["id"].eq(1), + table["id"].is_not_distinct_from(1), + table["id"].is_distinct_from(2), "sql literal", random_object ]) expected = WhereClause.new([ table["id"].not_in([1, 2, 3]), table["id"].not_eq(1), + table["id"].is_distinct_from(1), + table["id"].is_not_distinct_from(2), Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new("sql literal")), Arel::Nodes::Not.new(random_object) ]) diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index e471ee8039..756eeca35f 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -1315,6 +1315,13 @@ class RelationTest < ActiveRecord::TestCase assert_not_equal subscriber, Subscriber.create_or_find_by(nick: "cat") end + def test_create_or_find_by_should_not_raise_due_to_validation_errors + assert_nothing_raised do + bird = Bird.create_or_find_by(color: "green") + assert_predicate bird, :invalid? + end + end + def test_create_or_find_by_with_non_unique_attributes Subscriber.create!(nick: "bob", name: "the builder") @@ -1334,6 +1341,38 @@ class RelationTest < ActiveRecord::TestCase end end + def test_create_or_find_by_with_bang + assert_nil Subscriber.find_by(nick: "bob") + + subscriber = Subscriber.create!(nick: "bob") + + assert_equal subscriber, Subscriber.create_or_find_by!(nick: "bob") + assert_not_equal subscriber, Subscriber.create_or_find_by!(nick: "cat") + end + + def test_create_or_find_by_with_bang_should_raise_due_to_validation_errors + assert_raises(ActiveRecord::RecordInvalid) { Bird.create_or_find_by!(color: "green") } + end + + def test_create_or_find_by_with_bang_with_non_unique_attributes + Subscriber.create!(nick: "bob", name: "the builder") + + assert_raises(ActiveRecord::RecordNotFound) do + Subscriber.create_or_find_by!(nick: "bob", name: "the cat") + end + end + + def test_create_or_find_by_with_bang_within_transaction + assert_nil Subscriber.find_by(nick: "bob") + + subscriber = Subscriber.create!(nick: "bob") + + Subscriber.transaction do + assert_equal subscriber, Subscriber.create_or_find_by!(nick: "bob") + assert_not_equal subscriber, Subscriber.create_or_find_by!(nick: "cat") + end + end + def test_find_or_initialize_by assert_nil Bird.find_by(name: "bob") diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index db13f20a39..dda3efa47c 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -229,11 +229,14 @@ class SchemaDumperTest < ActiveRecord::TestCase if ActiveRecord::Base.connection.supports_expression_index? def test_schema_dump_expression_indices index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_expression_index/).first.strip + index_definition.sub!(/, name: "company_expression_index"\z/, "") if current_adapter?(:PostgreSQLAdapter) - assert_match %r{CASE.+lower\(\(name\)::text\)}i, index_definition + assert_match %r{CASE.+lower\(\(name\)::text\).+END\) DESC"\z}i, index_definition + elsif current_adapter?(:Mysql2Adapter) + assert_match %r{CASE.+lower\(`name`\).+END\) DESC"\z}i, index_definition elsif current_adapter?(:SQLite3Adapter) - assert_match %r{CASE.+lower\(name\)}i, index_definition + assert_match %r{CASE.+lower\(name\).+END\) DESC"\z}i, index_definition else assert false end diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index b4f4379e5e..b1f2ffe29c 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -254,11 +254,16 @@ class RelationScopingTest < ActiveRecord::TestCase end end - def test_scoping_works_in_the_scope_block + def test_scoping_with_klass_method_works_in_the_scope_block expected = SpecialPostWithDefaultScope.unscoped.to_a assert_equal expected, SpecialPostWithDefaultScope.unscoped_all end + def test_scoping_with_query_method_works_in_the_scope_block + expected = SpecialPostWithDefaultScope.unscoped.where(author_id: 0).to_a + assert_equal expected, SpecialPostWithDefaultScope.authorless + end + def test_circular_joins_with_scoping_does_not_crash posts = Post.joins(comments: :post).scoping do Post.first(10) diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 1192b30b14..f6cd4f85ee 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -13,6 +13,10 @@ class SerializedAttributeTest < ActiveRecord::TestCase MyObject = Struct.new :attribute1, :attribute2 + # NOTE: Use a duplicate of Topic so attribute + # changes don't bleed into other tests + Topic = ::Topic.dup + teardown do Topic.serialize("content") end @@ -367,7 +371,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase end def test_serialized_attribute_works_under_concurrent_initial_access - model = Topic.dup + model = ::Topic.dup topic = model.last topic.update group: "1" diff --git a/activerecord/test/cases/statement_invalid_test.rb b/activerecord/test/cases/statement_invalid_test.rb new file mode 100644 index 0000000000..16ea69c1bd --- /dev/null +++ b/activerecord/test/cases/statement_invalid_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/book" + +module ActiveRecord + class StatementInvalidTest < ActiveRecord::TestCase + fixtures :books + + class MockDatabaseError < StandardError + def result + 0 + end + + def error_number + 0 + end + end + + test "message contains no sql" do + sql = Book.where(author_id: 96, cover: "hard").to_sql + error = assert_raises(ActiveRecord::StatementInvalid) do + Book.connection.send(:log, sql, Book.name) do + raise MockDatabaseError + end + end + assert_not error.message.include?("SELECT") + end + + test "statement and binds are set on select" do + sql = Book.where(author_id: 96, cover: "hard").to_sql + binds = [Minitest::Mock.new, Minitest::Mock.new] + error = assert_raises(ActiveRecord::StatementInvalid) do + Book.connection.send(:log, sql, Book.name, binds) do + raise MockDatabaseError + end + end + assert_equal error.sql, sql + assert_equal error.binds, binds + end + end +end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 4d6dff68f9..552e623fd4 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -272,7 +272,7 @@ if current_adapter?(:Mysql2Adapter) def test_db_retrieves_collation ActiveRecord::Base.stub(:connection, @connection) do - assert_called_with(@connection, :collation) do + assert_called(@connection, :collation) do ActiveRecord::Tasks::DatabaseTasks.collation @configuration end end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index c0be45eee7..aa6b7915a2 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -591,6 +591,17 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase assert_equal [:before_commit, :after_commit], @topic.history end + def test_commit_run_transactions_callbacks_with_nested_transactions + @topic.transaction do + @topic.transaction(requires_new: true) do + @topic.content = "foo" + @topic.save! + @topic.class.connection.add_transaction_record(@topic) + end + end + assert_equal [:before_commit, :after_commit], @topic.history + end + def test_rollback_does_not_run_transactions_callbacks_without_enrollment @topic.transaction do @topic.content = "foo" diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index 7e2d66c62a..36b9df7ba5 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -20,7 +20,7 @@ module ViewBehavior def setup super @connection = ActiveRecord::Base.connection - create_view "ebooks'", <<-SQL + create_view "ebooks'", <<~SQL SELECT id, name, status FROM books WHERE format = 'ebook' SQL end @@ -106,7 +106,7 @@ if ActiveRecord::Base.connection.supports_views? setup do @connection = ActiveRecord::Base.connection - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE VIEW paperbacks AS SELECT name, status FROM books WHERE format = 'paperback' SQL @@ -156,8 +156,7 @@ if ActiveRecord::Base.connection.supports_views? end # sqlite dose not support CREATE, INSERT, and DELETE for VIEW - if current_adapter?(:Mysql2Adapter, :SQLServerAdapter) || - current_adapter?(:PostgreSQLAdapter) && ActiveRecord::Base.connection.postgresql_version >= 90300 + if current_adapter?(:Mysql2Adapter, :SQLServerAdapter, :PostgreSQLAdapter) class UpdateableViewTest < ActiveRecord::TestCase self.use_transactional_tests = false @@ -169,7 +168,7 @@ if ActiveRecord::Base.connection.supports_views? setup do @connection = ActiveRecord::Base.connection - @connection.execute <<-SQL + @connection.execute <<~SQL CREATE VIEW printed_books AS SELECT id, name, status, format FROM books WHERE format = 'paperback' SQL @@ -207,8 +206,7 @@ if ActiveRecord::Base.connection.supports_views? end # end of `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter)` end # end of `if ActiveRecord::Base.connection.supports_views?` -if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) && - ActiveRecord::Base.connection.supports_materialized_views? +if ActiveRecord::Base.connection.supports_materialized_views? class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase include ViewBehavior |