aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/test/cases
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord/test/cases')
-rw-r--r--activerecord/test/cases/adapter_test.rb247
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb70
-rw-r--r--activerecord/test/cases/adapters/mysql2/annotate_test.rb37
-rw-r--r--activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql2/charset_collation_test.rb10
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb17
-rw-r--r--activerecord/test/cases/adapters/mysql2/count_deleted_rows_with_lock_test.rb28
-rw-r--r--activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb8
-rw-r--r--activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb149
-rw-r--r--activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb48
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql2/sp_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/virtual_column_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb10
-rw-r--r--activerecord/test/cases/adapters/postgresql/annotate_test.rb37
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb24
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb17
-rw-r--r--activerecord/test/cases/adapters/postgresql/composite_test.rb14
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb42
-rw-r--r--activerecord/test/cases/adapters/postgresql/create_unlogged_tables_test.rb74
-rw-r--r--activerecord/test/cases/adapters/postgresql/datatype_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/enum_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/extension_migration_test.rb13
-rw-r--r--activerecord/test/cases/adapters/postgresql/foreign_table_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/geometric_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb25
-rw-r--r--activerecord/test/cases/adapters/postgresql/infinity_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/money_test.rb15
-rw-r--r--activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb52
-rw-r--r--activerecord/test/cases/adapters/postgresql/partitions_test.rb22
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb66
-rw-r--r--activerecord/test/cases/adapters/postgresql/quoting_test.rb5
-rw-r--r--activerecord/test/cases/adapters/postgresql/range_test.rb716
-rw-r--r--activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/rename_table_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb38
-rw-r--r--activerecord/test/cases/adapters/postgresql/transaction_test.rb1
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb28
-rw-r--r--activerecord/test/cases/adapters/sqlite3/annotate_test.rb37
-rw-r--r--activerecord/test/cases/adapters/sqlite3/bind_parameter_test.rb20
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb40
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb157
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb18
-rw-r--r--activerecord/test/cases/aggregations_test.rb2
-rw-r--r--activerecord/test/cases/ar_schema_test.rb87
-rw-r--r--activerecord/test/cases/arel/attributes/attribute_test.rb1080
-rw-r--r--activerecord/test/cases/arel/attributes/math_test.rb83
-rw-r--r--activerecord/test/cases/arel/attributes_test.rb68
-rw-r--r--activerecord/test/cases/arel/collectors/bind_test.rb40
-rw-r--r--activerecord/test/cases/arel/collectors/composite_test.rb47
-rw-r--r--activerecord/test/cases/arel/collectors/sql_string_test.rb41
-rw-r--r--activerecord/test/cases/arel/collectors/substitute_bind_collector_test.rb48
-rw-r--r--activerecord/test/cases/arel/crud_test.rb65
-rw-r--r--activerecord/test/cases/arel/delete_manager_test.rb53
-rw-r--r--activerecord/test/cases/arel/factory_methods_test.rb46
-rw-r--r--activerecord/test/cases/arel/helper.rb45
-rw-r--r--activerecord/test/cases/arel/insert_manager_test.rb241
-rw-r--r--activerecord/test/cases/arel/nodes/and_test.rb30
-rw-r--r--activerecord/test/cases/arel/nodes/as_test.rb36
-rw-r--r--activerecord/test/cases/arel/nodes/ascending_test.rb46
-rw-r--r--activerecord/test/cases/arel/nodes/bin_test.rb35
-rw-r--r--activerecord/test/cases/arel/nodes/binary_test.rb29
-rw-r--r--activerecord/test/cases/arel/nodes/bind_param_test.rb22
-rw-r--r--activerecord/test/cases/arel/nodes/case_test.rb96
-rw-r--r--activerecord/test/cases/arel/nodes/casted_test.rb18
-rw-r--r--activerecord/test/cases/arel/nodes/comment_test.rb22
-rw-r--r--activerecord/test/cases/arel/nodes/count_test.rb35
-rw-r--r--activerecord/test/cases/arel/nodes/delete_statement_test.rb36
-rw-r--r--activerecord/test/cases/arel/nodes/descending_test.rb46
-rw-r--r--activerecord/test/cases/arel/nodes/distinct_test.rb21
-rw-r--r--activerecord/test/cases/arel/nodes/equality_test.rb86
-rw-r--r--activerecord/test/cases/arel/nodes/extract_test.rb43
-rw-r--r--activerecord/test/cases/arel/nodes/false_test.rb21
-rw-r--r--activerecord/test/cases/arel/nodes/grouping_test.rb26
-rw-r--r--activerecord/test/cases/arel/nodes/infix_operation_test.rb42
-rw-r--r--activerecord/test/cases/arel/nodes/insert_statement_test.rb44
-rw-r--r--activerecord/test/cases/arel/nodes/named_function_test.rb48
-rw-r--r--activerecord/test/cases/arel/nodes/node_test.rb41
-rw-r--r--activerecord/test/cases/arel/nodes/not_test.rb31
-rw-r--r--activerecord/test/cases/arel/nodes/or_test.rb36
-rw-r--r--activerecord/test/cases/arel/nodes/over_test.rb69
-rw-r--r--activerecord/test/cases/arel/nodes/select_core_test.rb79
-rw-r--r--activerecord/test/cases/arel/nodes/select_statement_test.rb51
-rw-r--r--activerecord/test/cases/arel/nodes/sql_literal_test.rb75
-rw-r--r--activerecord/test/cases/arel/nodes/sum_test.rb35
-rw-r--r--activerecord/test/cases/arel/nodes/table_alias_test.rb29
-rw-r--r--activerecord/test/cases/arel/nodes/true_test.rb21
-rw-r--r--activerecord/test/cases/arel/nodes/unary_operation_test.rb41
-rw-r--r--activerecord/test/cases/arel/nodes/update_statement_test.rb60
-rw-r--r--activerecord/test/cases/arel/nodes/window_test.rb81
-rw-r--r--activerecord/test/cases/arel/nodes_test.rb34
-rw-r--r--activerecord/test/cases/arel/select_manager_test.rb1248
-rw-r--r--activerecord/test/cases/arel/support/fake_record.rb133
-rw-r--r--activerecord/test/cases/arel/table_test.rb216
-rw-r--r--activerecord/test/cases/arel/update_manager_test.rb126
-rw-r--r--activerecord/test/cases/arel/visitors/depth_first_test.rb276
-rw-r--r--activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb72
-rw-r--r--activerecord/test/cases/arel/visitors/dot_test.rb83
-rw-r--r--activerecord/test/cases/arel/visitors/ibm_db_test.rb73
-rw-r--r--activerecord/test/cases/arel/visitors/informix_test.rb98
-rw-r--r--activerecord/test/cases/arel/visitors/mssql_test.rb138
-rw-r--r--activerecord/test/cases/arel/visitors/mysql_test.rb109
-rw-r--r--activerecord/test/cases/arel/visitors/oracle12_test.rb121
-rw-r--r--activerecord/test/cases/arel/visitors/oracle_test.rb257
-rw-r--r--activerecord/test/cases/arel/visitors/postgres_test.rb320
-rw-r--r--activerecord/test/cases/arel/visitors/sqlite_test.rb75
-rw-r--r--activerecord/test/cases/arel/visitors/to_sql_test.rb713
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb210
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb23
-rw-r--r--activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb2
-rw-r--r--activerecord/test/cases/associations/eager_load_nested_include_test.rb2
-rw-r--r--activerecord/test/cases/associations/eager_test.rb157
-rw-r--r--activerecord/test/cases/associations/extension_test.rb2
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb72
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb461
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb151
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb74
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb25
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb11
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb4
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb2
-rw-r--r--activerecord/test/cases/associations/left_outer_join_association_test.rb10
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb19
-rw-r--r--activerecord/test/cases/associations/required_test.rb50
-rw-r--r--activerecord/test/cases/associations_test.rb107
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb137
-rw-r--r--activerecord/test/cases/attributes_test.rb25
-rw-r--r--activerecord/test/cases/autosave_association_test.rb207
-rw-r--r--activerecord/test/cases/base_test.rb176
-rw-r--r--activerecord/test/cases/batches_test.rb34
-rw-r--r--activerecord/test/cases/binary_test.rb2
-rw-r--r--activerecord/test/cases/bind_parameter_test.rb117
-rw-r--r--activerecord/test/cases/boolean_test.rb52
-rw-r--r--activerecord/test/cases/cache_key_test.rb82
-rw-r--r--activerecord/test/cases/calculations_test.rb136
-rw-r--r--activerecord/test/cases/callbacks_test.rb55
-rw-r--r--activerecord/test/cases/clone_test.rb4
-rw-r--r--activerecord/test/cases/collection_cache_key_test.rb18
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handler_test.rb43
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb391
-rw-r--r--activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb25
-rw-r--r--activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb4
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb81
-rw-r--r--activerecord/test/cases/connection_management_test.rb2
-rw-r--r--activerecord/test/cases/connection_pool_test.rb121
-rw-r--r--activerecord/test/cases/connection_specification/resolver_test.rb8
-rw-r--r--activerecord/test/cases/core_test.rb20
-rw-r--r--activerecord/test/cases/counter_cache_test.rb14
-rw-r--r--activerecord/test/cases/database_configurations_test.rb117
-rw-r--r--activerecord/test/cases/database_selector_test.rb166
-rw-r--r--activerecord/test/cases/date_test.rb14
-rw-r--r--activerecord/test/cases/date_time_precision_test.rb24
-rw-r--r--activerecord/test/cases/defaults_test.rb41
-rw-r--r--activerecord/test/cases/dirty_test.rb50
-rw-r--r--activerecord/test/cases/dup_test.rb8
-rw-r--r--activerecord/test/cases/enum_test.rb57
-rw-r--r--activerecord/test/cases/errors_test.rb8
-rw-r--r--activerecord/test/cases/explain_subscriber_test.rb2
-rw-r--r--activerecord/test/cases/filter_attributes_test.rb132
-rw-r--r--activerecord/test/cases/finder_respond_to_test.rb6
-rw-r--r--activerecord/test/cases/finder_test.rb131
-rw-r--r--activerecord/test/cases/fixtures_test.rb293
-rw-r--r--activerecord/test/cases/forbidden_attributes_protection_test.rb38
-rw-r--r--activerecord/test/cases/habtm_destroy_order_test.rb28
-rw-r--r--activerecord/test/cases/helper.rb25
-rw-r--r--activerecord/test/cases/hot_compatibility_test.rb2
-rw-r--r--activerecord/test/cases/inheritance_test.rb10
-rw-r--r--activerecord/test/cases/insert_all_test.rb276
-rw-r--r--activerecord/test/cases/instrumentation_test.rb12
-rw-r--r--activerecord/test/cases/integration_test.rb89
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb44
-rw-r--r--activerecord/test/cases/locking_test.rb24
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb15
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb23
-rw-r--r--activerecord/test/cases/migration/change_table_test.rb8
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb8
-rw-r--r--activerecord/test/cases/migration/columns_test.rb25
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb29
-rw-r--r--activerecord/test/cases/migration/compatibility_test.rb114
-rw-r--r--activerecord/test/cases/migration/create_join_table_test.rb14
-rw-r--r--activerecord/test/cases/migration/foreign_key_test.rb213
-rw-r--r--activerecord/test/cases/migration/index_test.rb26
-rw-r--r--activerecord/test/cases/migration/pending_migrations_test.rb4
-rw-r--r--activerecord/test/cases/migration/references_foreign_key_test.rb61
-rw-r--r--activerecord/test/cases/migration_test.rb187
-rw-r--r--activerecord/test/cases/migrator_test.rb1
-rw-r--r--activerecord/test/cases/modules_test.rb6
-rw-r--r--activerecord/test/cases/multiple_db_test.rb12
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb29
-rw-r--r--activerecord/test/cases/null_relation_test.rb15
-rw-r--r--activerecord/test/cases/numeric_data_test.rb34
-rw-r--r--activerecord/test/cases/persistence_test.rb259
-rw-r--r--activerecord/test/cases/pooled_connections_test.rb28
-rw-r--r--activerecord/test/cases/primary_keys_test.rb3
-rw-r--r--activerecord/test/cases/query_cache_test.rb226
-rw-r--r--activerecord/test/cases/quoting_test.rb30
-rw-r--r--activerecord/test/cases/readonly_test.rb12
-rw-r--r--activerecord/test/cases/reaper_test.rb12
-rw-r--r--activerecord/test/cases/reflection_test.rb2
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb52
-rw-r--r--activerecord/test/cases/relation/delete_all_test.rb102
-rw-r--r--activerecord/test/cases/relation/merging_test.rb30
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb4
-rw-r--r--activerecord/test/cases/relation/or_test.rb12
-rw-r--r--activerecord/test/cases/relation/select_test.rb14
-rw-r--r--activerecord/test/cases/relation/update_all_test.rb324
-rw-r--r--activerecord/test/cases/relation/where_clause_test.rb4
-rw-r--r--activerecord/test/cases/relation/where_test.rb41
-rw-r--r--activerecord/test/cases/relation_test.rb85
-rw-r--r--activerecord/test/cases/relations_test.rb407
-rw-r--r--activerecord/test/cases/result_test.rb19
-rw-r--r--activerecord/test/cases/sanitize_test.rb19
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb102
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb28
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb37
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb82
-rw-r--r--activerecord/test/cases/serialization_test.rb4
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb35
-rw-r--r--activerecord/test/cases/statement_cache_test.rb6
-rw-r--r--activerecord/test/cases/statement_invalid_test.rb42
-rw-r--r--activerecord/test/cases/store_test.rb102
-rw-r--r--activerecord/test/cases/suppressor_test.rb2
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb1012
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb302
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb451
-rw-r--r--activerecord/test/cases/tasks/sqlite_rake_test.rb157
-rw-r--r--activerecord/test/cases/test_case.rb8
-rw-r--r--activerecord/test/cases/time_precision_test.rb22
-rw-r--r--activerecord/test/cases/timestamp_test.rb4
-rw-r--r--activerecord/test/cases/touch_later_test.rb4
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb60
-rw-r--r--activerecord/test/cases/transaction_isolation_test.rb6
-rw-r--r--activerecord/test/cases/transactions_test.rb243
-rw-r--r--activerecord/test/cases/type/time_test.rb22
-rw-r--r--activerecord/test/cases/type/type_map_test.rb6
-rw-r--r--activerecord/test/cases/unconnected_test.rb16
-rw-r--r--activerecord/test/cases/validations/absence_validation_test.rb2
-rw-r--r--activerecord/test/cases/validations/i18n_generate_message_validation_test.rb20
-rw-r--r--activerecord/test/cases/validations/length_validation_test.rb10
-rw-r--r--activerecord/test/cases/validations/presence_validation_test.rb2
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb95
-rw-r--r--activerecord/test/cases/validations_test.rb29
-rw-r--r--activerecord/test/cases/view_test.rb12
244 files changed, 16853 insertions, 2920 deletions
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 4c41a68407..ba04859bf0 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "cases/helper"
+require "support/connection_helper"
require "models/book"
require "models/post"
require "models/author"
@@ -10,6 +11,7 @@ module ActiveRecord
class AdapterTest < ActiveRecord::TestCase
def setup
@connection = ActiveRecord::Base.connection
+ @connection.materialize_transactions
end
##
@@ -84,7 +86,7 @@ module ActiveRecord
indexes = @connection.indexes("accounts")
assert_equal "accounts", indexes.first.table
assert_equal idx_name, indexes.first.name
- assert !indexes.first.unique
+ assert_not indexes.first.unique
assert_equal ["firm_id"], indexes.first.columns
ensure
@connection.remove_index(:accounts, name: idx_name) rescue nil
@@ -107,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
@@ -125,19 +132,17 @@ module ActiveRecord
end
def test_not_specifying_database_name_for_cross_database_selects
- begin
- assert_nothing_raised do
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"].except(:database))
-
- config = ARTest.connection_config
- ActiveRecord::Base.connection.execute(
- "SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \
- "FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses"
- )
- end
- ensure
- ActiveRecord::Base.establish_connection :arunit
+ assert_nothing_raised do
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"].except(:database))
+
+ config = ARTest.connection_config
+ ActiveRecord::Base.connection.execute(
+ "SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \
+ "FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses"
+ )
end
+ ensure
+ ActiveRecord::Base.establish_connection :arunit
end
end
@@ -158,6 +163,65 @@ module ActiveRecord
end
end
+ def test_preventing_writes_predicate
+ assert_not_predicate @connection, :preventing_writes?
+
+ @connection.while_preventing_writes do
+ assert_predicate @connection, :preventing_writes?
+ end
+
+ assert_not_predicate @connection, :preventing_writes?
+ end
+
+ def test_errors_when_an_insert_query_is_called_while_preventing_writes
+ assert_no_queries do
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @connection.while_preventing_writes do
+ @connection.transaction do
+ @connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')", nil, false)
+ end
+ end
+ end
+ end
+ end
+
+ def test_errors_when_an_update_query_is_called_while_preventing_writes
+ @connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
+
+ assert_no_queries do
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @connection.while_preventing_writes do
+ @connection.transaction do
+ @connection.update("UPDATE subscribers SET nick = '9989' WHERE nick = '138853948594'")
+ end
+ end
+ end
+ end
+ end
+
+ def test_errors_when_a_delete_query_is_called_while_preventing_writes
+ @connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
+
+ assert_no_queries do
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @connection.while_preventing_writes do
+ @connection.transaction do
+ @connection.delete("DELETE FROM subscribers WHERE nick = '138853948594'")
+ end
+ end
+ end
+ end
+ end
+
+ def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes
+ @connection.insert("INSERT INTO subscribers(nick) VALUES ('138853948594')")
+
+ @connection.while_preventing_writes do
+ result = @connection.select_all("SELECT subscribers.* FROM subscribers WHERE nick = '138853948594'")
+ assert_equal 1, result.length
+ end
+ end
+
def test_uniqueness_violations_are_translated_to_specific_exception
@connection.execute "INSERT INTO subscribers(nick) VALUES('me')"
error = assert_raises(ActiveRecord::RecordNotUnique) do
@@ -225,7 +289,7 @@ module ActiveRecord
post = Post.create!(title: "foo", body: "bar")
expected = @connection.select_all("SELECT * FROM posts WHERE id = #{post.id}")
result = @connection.select_all("SELECT * FROM posts WHERE id = #{Arel::Nodes::BindParam.new(nil).to_sql}", nil, [[nil, post.id]])
- assert_equal expected.to_hash, result.to_hash
+ assert_equal expected.to_a, result.to_a
end
def test_insert_update_delete_with_legacy_binds
@@ -284,22 +348,48 @@ 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 "ы".dup.force_encoding(Encoding::ASCII_8BIT)
- end
- end
+ def test_supports_foreign_keys_in_create_is_deprecated
+ assert_deprecated { @connection.supports_foreign_keys_in_create? }
+ end
- assert_equal "ы", error.message
- end
+ def test_supports_multi_insert_is_deprecated
+ assert_deprecated { @connection.supports_multi_insert? }
+ end
+
+ def test_column_name_length_is_deprecated
+ assert_deprecated { @connection.column_name_length }
+ end
+
+ def test_table_name_length_is_deprecated
+ assert_deprecated { @connection.table_name_length }
+ end
+
+ def test_columns_per_table_is_deprecated
+ assert_deprecated { @connection.columns_per_table }
+ end
+
+ def test_indexes_per_table_is_deprecated
+ assert_deprecated { @connection.indexes_per_table }
+ end
+
+ def test_columns_per_multicolumn_index_is_deprecated
+ assert_deprecated { @connection.columns_per_multicolumn_index }
+ end
+
+ def test_sql_query_length_is_deprecated
+ assert_deprecated { @connection.sql_query_length }
+ end
+
+ def test_joins_per_query_is_deprecated
+ assert_deprecated { @connection.joins_per_query }
end
end
class AdapterForeignKeyTest < ActiveRecord::TestCase
self.use_transactional_tests = false
+ fixtures :fk_test_has_pk
+
def setup
@connection = ActiveRecord::Base.connection
end
@@ -318,7 +408,7 @@ module ActiveRecord
assert_not_nil error.cause
end
- def test_foreign_key_violations_are_translated_to_specific_exception
+ def test_foreign_key_violations_on_insert_are_translated_to_specific_exception
error = assert_raises(ActiveRecord::InvalidForeignKey) do
insert_into_fk_test_has_fk
end
@@ -326,6 +416,16 @@ module ActiveRecord
assert_not_nil error.cause
end
+ def test_foreign_key_violations_on_delete_are_translated_to_specific_exception
+ insert_into_fk_test_has_fk fk_id: 1
+
+ error = assert_raises(ActiveRecord::InvalidForeignKey) do
+ @connection.execute "DELETE FROM fk_test_has_pk WHERE pk_id = 1"
+ end
+
+ assert_not_nil error.cause
+ end
+
def test_disable_referential_integrity
assert_nothing_raised do
@connection.disable_referential_integrity do
@@ -338,14 +438,13 @@ module ActiveRecord
end
private
-
- def insert_into_fk_test_has_fk
+ def insert_into_fk_test_has_fk(fk_id: 0)
# Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method
if @connection.prefetch_primary_key?
id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id"))
- @connection.execute "INSERT INTO fk_test_has_fk (id,fk_id) VALUES (#{id_value},0)"
+ @connection.execute "INSERT INTO fk_test_has_fk (id,fk_id) VALUES (#{id_value},#{fk_id})"
else
- @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)"
+ @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (#{fk_id})"
end
end
end
@@ -353,19 +452,21 @@ module ActiveRecord
class AdapterTestWithoutTransaction < ActiveRecord::TestCase
self.use_transactional_tests = false
- class Klass < ActiveRecord::Base
- end
+ fixtures :posts, :authors, :author_addresses
def setup
- Klass.establish_connection :arunit
- @connection = Klass.connection
- end
-
- teardown do
- Klass.remove_connection
+ @connection = ActiveRecord::Base.connection
end
unless in_memory_db?
+ test "reconnect after a disconnect" do
+ assert_predicate @connection, :active?
+ @connection.disconnect!
+ assert_not_predicate @connection, :active?
+ @connection.reconnect!
+ assert_predicate @connection, :active?
+ end
+
test "transaction state is reset after a reconnect" do
@connection.begin_transaction
assert_predicate @connection, :transaction_open?
@@ -378,9 +479,59 @@ module ActiveRecord
assert_predicate @connection, :transaction_open?
@connection.disconnect!
assert_not_predicate @connection, :transaction_open?
+ ensure
+ @connection.reconnect!
end
end
+ def test_truncate
+ assert_operator Post.count, :>, 0
+
+ @connection.truncate("posts")
+
+ assert_equal 0, Post.count
+ end
+
+ def test_truncate_with_query_cache
+ @connection.enable_query_cache!
+
+ assert_operator Post.count, :>, 0
+
+ @connection.truncate("posts")
+
+ assert_equal 0, Post.count
+ ensure
+ @connection.disable_query_cache!
+ end
+
+ def test_truncate_tables
+ assert_operator Post.count, :>, 0
+ assert_operator Author.count, :>, 0
+ assert_operator AuthorAddress.count, :>, 0
+
+ @connection.truncate_tables("author_addresses", "authors", "posts")
+
+ assert_equal 0, Post.count
+ assert_equal 0, Author.count
+ assert_equal 0, AuthorAddress.count
+ end
+
+ def test_truncate_tables_with_query_cache
+ @connection.enable_query_cache!
+
+ assert_operator Post.count, :>, 0
+ assert_operator Author.count, :>, 0
+ assert_operator AuthorAddress.count, :>, 0
+
+ @connection.truncate_tables("author_addresses", "authors", "posts")
+
+ assert_equal 0, Post.count
+ assert_equal 0, Author.count
+ assert_equal 0, AuthorAddress.count
+ ensure
+ @connection.disable_query_cache!
+ end
+
# test resetting sequences in odd tables in PostgreSQL
if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
require "models/movie"
@@ -402,3 +553,27 @@ module ActiveRecord
end
end
end
+
+if ActiveRecord::Base.connection.supports_advisory_locks?
+ class AdvisoryLocksEnabledTest < ActiveRecord::TestCase
+ include ConnectionHelper
+
+ def test_advisory_locks_enabled?
+ assert ActiveRecord::Base.connection.advisory_locks_enabled?
+
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(
+ orig_connection.merge(advisory_locks: false)
+ )
+
+ assert_not ActiveRecord::Base.connection.advisory_locks_enabled?
+
+ ActiveRecord::Base.establish_connection(
+ orig_connection.merge(advisory_locks: true)
+ )
+
+ assert ActiveRecord::Base.connection.advisory_locks_enabled?
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
index 9ae2c42368..88c2ac5d0a 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -7,6 +7,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
include ConnectionHelper
def setup
+ ActiveRecord::Base.connection.send(:default_row_format)
ActiveRecord::Base.connection.singleton_class.class_eval do
alias_method :execute_without_stub, :execute
def execute(sql, name = nil) sql end
@@ -68,18 +69,18 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
def (ActiveRecord::Base.connection).data_source_exists?(*); false; end
%w(SPATIAL FULLTEXT UNIQUE).each do |type|
- expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`))"
+ expected = /\ACREATE TABLE `people` \(#{type} INDEX `index_people_on_last_name` \(`last_name`\)\)/
actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t|
t.index :last_name, type: type
end
- assert_equal expected, actual
+ assert_match expected, actual
end
- expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)))"
+ expected = /\ACREATE TABLE `people` \( INDEX `index_people_on_last_name` USING btree \(`last_name`\(10\)\)\)/
actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t|
t.index :last_name, length: 10, using: :btree
end
- assert_equal expected, actual
+ assert_match expected, actual
end
def test_index_in_bulk_change
@@ -106,7 +107,13 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
end
def test_create_mysql_database_with_encoding
- assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
+ if row_format_dynamic_by_default?
+ assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8mb4`", create_database(:matt)
+ else
+ error = assert_raises(RuntimeError) { create_database(:matt) }
+ expected = "Configure a supported :charset and ensure innodb_large_prefix is enabled to support indexes on varchar(255) string columns."
+ assert_equal expected, error.message
+ end
assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, charset: "latin1")
assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT COLLATE `utf8mb4_bin`", create_database(:matt_aimonetti, collation: "utf8mb4_bin")
end
@@ -130,42 +137,42 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
def test_add_timestamps
with_real_execute do
- begin
- ActiveRecord::Base.connection.create_table :delete_me
- ActiveRecord::Base.connection.add_timestamps :delete_me, null: true
- assert column_present?("delete_me", "updated_at", "datetime")
- assert column_present?("delete_me", "created_at", "datetime")
- ensure
- ActiveRecord::Base.connection.drop_table :delete_me rescue nil
- end
+ ActiveRecord::Base.connection.create_table :delete_me
+ ActiveRecord::Base.connection.add_timestamps :delete_me, null: true
+ assert column_exists?("delete_me", "updated_at", "datetime")
+ assert column_exists?("delete_me", "created_at", "datetime")
+ ensure
+ ActiveRecord::Base.connection.drop_table :delete_me rescue nil
end
end
def test_remove_timestamps
with_real_execute do
- begin
- ActiveRecord::Base.connection.create_table :delete_me do |t|
- t.timestamps null: true
- end
- ActiveRecord::Base.connection.remove_timestamps :delete_me, null: true
- assert !column_present?("delete_me", "updated_at", "datetime")
- assert !column_present?("delete_me", "created_at", "datetime")
- ensure
- ActiveRecord::Base.connection.drop_table :delete_me rescue nil
+ ActiveRecord::Base.connection.create_table :delete_me do |t|
+ t.timestamps null: true
end
+ ActiveRecord::Base.connection.remove_timestamps :delete_me, null: true
+ assert_not column_exists?("delete_me", "updated_at", "datetime")
+ assert_not column_exists?("delete_me", "created_at", "datetime")
+ ensure
+ ActiveRecord::Base.connection.drop_table :delete_me rescue nil
end
end
def test_indexes_in_create
- ActiveRecord::Base.connection.stubs(:data_source_exists?).with(:temp).returns(false)
- ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)
+ assert_called_with(
+ ActiveRecord::Base.connection,
+ :data_source_exists?,
+ [:temp],
+ returns: false
+ ) do
+ expected = /\ACREATE TEMPORARY TABLE `temp` \( INDEX `index_temp_on_zip` \(`zip`\)\)(?: ROW_FORMAT=DYNAMIC)? AS SELECT id, name, zip FROM a_really_complicated_query/
+ actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t|
+ t.index :zip
+ end
- expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`)) AS SELECT id, name, zip FROM a_really_complicated_query"
- actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t|
- t.index :zip
+ assert_match expected, actual
end
-
- assert_equal expected, actual
end
private
@@ -187,9 +194,4 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
def method_missing(method_symbol, *arguments)
ActiveRecord::Base.connection.send(method_symbol, *arguments)
end
-
- def column_present?(table_name, column_name, type)
- results = ActiveRecord::Base.connection.select_all("SHOW FIELDS FROM #{table_name} LIKE '#{column_name}'")
- results.first && results.first["Type"] == type
- end
end
diff --git a/activerecord/test/cases/adapters/mysql2/annotate_test.rb b/activerecord/test/cases/adapters/mysql2/annotate_test.rb
new file mode 100644
index 0000000000..b512540073
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/annotate_test.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+class Mysql2AnnotateTest < ActiveRecord::Mysql2TestCase
+ fixtures :posts
+
+ def test_annotate_wraps_content_in_an_inline_comment
+ assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/}) do
+ posts = Post.select(:id).annotate("foo")
+ assert posts.first
+ end
+ end
+
+ def test_annotate_is_sanitized
+ assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/}) do
+ posts = Post.select(:id).annotate("*/foo/*")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/}) do
+ posts = Post.select(:id).annotate("**//foo//**")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* foo \*/ /\* bar \*/}) do
+ posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT `posts`\.`id` FROM `posts` /\* \+ MAX_EXECUTION_TIME\(1\) \*/}) do
+ posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)")
+ assert posts.first
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
index aa870349be..c32475c683 100644
--- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
@@ -9,8 +9,8 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase
repair_validations(CollationTest)
def test_columns_include_collation_different_from_table
- assert_equal "utf8_bin", CollationTest.columns_hash["string_cs_column"].collation
- assert_equal "utf8_general_ci", CollationTest.columns_hash["string_ci_column"].collation
+ assert_equal "utf8mb4_bin", CollationTest.columns_hash["string_cs_column"].collation
+ assert_equal "utf8mb4_general_ci", CollationTest.columns_hash["string_ci_column"].collation
end
def test_case_sensitive
diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb
index d0c57de65d..0bdbefdfb9 100644
--- a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb
@@ -32,20 +32,20 @@ class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase
end
test "add column with charset and collation" do
- @connection.add_column :charset_collations, :title, :string, charset: "utf8", collation: "utf8_bin"
+ @connection.add_column :charset_collations, :title, :string, charset: "utf8mb4", collation: "utf8mb4_bin"
column = @connection.columns(:charset_collations).find { |c| c.name == "title" }
assert_equal :string, column.type
- assert_equal "utf8_bin", column.collation
+ assert_equal "utf8mb4_bin", column.collation
end
test "change column with charset and collation" do
- @connection.add_column :charset_collations, :description, :string, charset: "utf8", collation: "utf8_unicode_ci"
- @connection.change_column :charset_collations, :description, :text, charset: "utf8", collation: "utf8_general_ci"
+ @connection.add_column :charset_collations, :description, :string, charset: "utf8mb4", collation: "utf8mb4_unicode_ci"
+ @connection.change_column :charset_collations, :description, :text, charset: "utf8mb4", collation: "utf8mb4_general_ci"
column = @connection.columns(:charset_collations).find { |c| c.name == "description" }
assert_equal :text, column.type
- assert_equal "utf8_general_ci", column.collation
+ assert_equal "utf8mb4_general_ci", column.collation
end
test "schema dump includes collation" do
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index 726f58d58e..9c6566106a 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -28,17 +28,6 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
end
end
- def test_truncate
- rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments")
- count = rows.first.values.first
- assert_operator count, :>, 0
-
- ActiveRecord::Base.connection.truncate("comments")
- rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments")
- count = rows.first.values.first
- assert_equal 0, count
- end
-
def test_no_automatic_reconnection_after_timeout
assert_predicate @connection, :active?
@connection.update("set @@wait_timeout=1")
@@ -104,8 +93,8 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
end
def test_mysql_connection_collation_is_configured
- assert_equal "utf8_unicode_ci", @connection.show_variable("collation_connection")
- assert_equal "utf8_general_ci", ARUnit2Model.connection.show_variable("collation_connection")
+ assert_equal "utf8mb4_unicode_ci", @connection.show_variable("collation_connection")
+ assert_equal "utf8mb4_general_ci", ARUnit2Model.connection.show_variable("collation_connection")
end
def test_mysql_default_in_strict_mode
@@ -170,6 +159,8 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
end
def test_logs_name_show_variable
+ ActiveRecord::Base.connection.materialize_transactions
+ @subscriber.logged.clear
@connection.show_variable "foo"
assert_equal "SCHEMA", @subscriber.logged[0][1]
end
diff --git a/activerecord/test/cases/adapters/mysql2/count_deleted_rows_with_lock_test.rb b/activerecord/test/cases/adapters/mysql2/count_deleted_rows_with_lock_test.rb
new file mode 100644
index 0000000000..4d361e405c
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/count_deleted_rows_with_lock_test.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/connection_helper"
+require "models/author"
+require "models/bulb"
+
+module ActiveRecord
+ class CountDeletedRowsWithLockTest < ActiveRecord::Mysql2TestCase
+ test "delete and create in different threads synchronize correctly" do
+ Bulb.unscoped.delete_all
+ Bulb.create!(name: "Jimmy", color: "blue")
+
+ delete_thread = Thread.new do
+ Bulb.unscoped.delete_all
+ end
+
+ create_thread = Thread.new do
+ Author.create!(name: "Tommy")
+ end
+
+ delete_thread.join
+ create_thread.join
+
+ assert_equal 1, delete_thread.value
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb
index fa54f39992..cbe55f1d53 100644
--- a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb
@@ -45,10 +45,8 @@ class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase
end
def stub_version(full_version_string)
- @connection.stubs(:full_version).returns(full_version_string)
- @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
- yield
- ensure
- @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version)
+ @connection.stub(:full_version, full_version_string) do
+ yield
+ end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
index 0719baaa23..6ade2eec24 100644
--- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
@@ -56,7 +56,7 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
@conn.columns_for_distinct("posts.id", [order])
end
- def test_errors_for_bigint_fks_on_integer_pk_table
+ def test_errors_for_bigint_fks_on_integer_pk_table_in_alter_table
# table old_cars has primary key of integer
error = assert_raises(ActiveRecord::MismatchedForeignKey) do
@@ -64,9 +64,152 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
@conn.add_foreign_key :engines, :old_cars
end
- assert_match "Column `old_car_id` on table `engines` has a type of `bigint(20)`", error.message
+ assert_includes error.message, <<~MSG.squish
+ Column `old_car_id` on table `engines` does not match column `id` on `old_cars`,
+ which has type `int(11)`. To resolve this issue, change the type of the `old_car_id`
+ column on `engines` to be :integer. (For example `t.integer :old_car_id`).
+ MSG
assert_not_nil error.cause
- @conn.exec_query("ALTER TABLE engines DROP COLUMN old_car_id")
+ ensure
+ @conn.execute("ALTER TABLE engines DROP COLUMN old_car_id") rescue nil
+ end
+
+ def test_errors_for_bigint_fks_on_integer_pk_table_in_create_table
+ # table old_cars has primary key of integer
+
+ error = assert_raises(ActiveRecord::MismatchedForeignKey) do
+ @conn.execute(<<~SQL)
+ CREATE TABLE activerecord_unittest.foos (
+ id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ old_car_id bigint,
+ INDEX index_foos_on_old_car_id (old_car_id),
+ CONSTRAINT fk_rails_ff771f3c96 FOREIGN KEY (old_car_id) REFERENCES old_cars (id)
+ )
+ SQL
+ end
+
+ assert_includes error.message, <<~MSG.squish
+ Column `old_car_id` on table `foos` does not match column `id` on `old_cars`,
+ which has type `int(11)`. To resolve this issue, change the type of the `old_car_id`
+ column on `foos` to be :integer. (For example `t.integer :old_car_id`).
+ MSG
+ assert_not_nil error.cause
+ ensure
+ @conn.drop_table :foos, if_exists: true
+ end
+
+ def test_errors_for_integer_fks_on_bigint_pk_table_in_create_table
+ # table old_cars has primary key of bigint
+
+ error = assert_raises(ActiveRecord::MismatchedForeignKey) do
+ @conn.execute(<<~SQL)
+ CREATE TABLE activerecord_unittest.foos (
+ id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ car_id int,
+ INDEX index_foos_on_car_id (car_id),
+ CONSTRAINT fk_rails_ff771f3c96 FOREIGN KEY (car_id) REFERENCES cars (id)
+ )
+ SQL
+ end
+
+ assert_includes error.message, <<~MSG.squish
+ Column `car_id` on table `foos` does not match column `id` on `cars`,
+ which has type `bigint(20)`. To resolve this issue, change the type of the `car_id`
+ column on `foos` to be :bigint. (For example `t.bigint :car_id`).
+ MSG
+ assert_not_nil error.cause
+ ensure
+ @conn.drop_table :foos, if_exists: true
+ end
+
+ def test_errors_for_bigint_fks_on_string_pk_table_in_create_table
+ # table old_cars has primary key of string
+
+ error = assert_raises(ActiveRecord::MismatchedForeignKey) do
+ @conn.execute(<<~SQL)
+ CREATE TABLE activerecord_unittest.foos (
+ id bigint NOT NULL AUTO_INCREMENT PRIMARY KEY,
+ subscriber_id bigint,
+ INDEX index_foos_on_subscriber_id (subscriber_id),
+ CONSTRAINT fk_rails_ff771f3c96 FOREIGN KEY (subscriber_id) REFERENCES subscribers (nick)
+ )
+ SQL
+ end
+
+ assert_includes error.message, <<~MSG.squish
+ Column `subscriber_id` on table `foos` does not match column `nick` on `subscribers`,
+ which has type `varchar(255)`. To resolve this issue, change the type of the `subscriber_id`
+ column on `foos` to be :string. (For example `t.string :subscriber_id`).
+ MSG
+ assert_not_nil error.cause
+ ensure
+ @conn.drop_table :foos, if_exists: true
+ end
+
+ def test_errors_when_an_insert_query_is_called_while_preventing_writes
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @conn.while_preventing_writes do
+ @conn.insert("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')")
+ end
+ end
+ end
+
+ def test_errors_when_an_update_query_is_called_while_preventing_writes
+ @conn.insert("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')")
+
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @conn.while_preventing_writes do
+ @conn.update("UPDATE `engines` SET `engines`.`car_id` = '9989' WHERE `engines`.`car_id` = '138853948594'")
+ end
+ end
+ end
+
+ def test_errors_when_a_delete_query_is_called_while_preventing_writes
+ @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')")
+
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @conn.while_preventing_writes do
+ @conn.execute("DELETE FROM `engines` where `engines`.`car_id` = '138853948594'")
+ end
+ end
+ end
+
+ def test_errors_when_a_replace_query_is_called_while_preventing_writes
+ @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')")
+
+ assert_raises(ActiveRecord::ReadOnlyError) do
+ @conn.while_preventing_writes do
+ @conn.execute("REPLACE INTO `engines` SET `engines`.`car_id` = '249823948'")
+ end
+ end
+ end
+
+ def test_doesnt_error_when_a_select_query_is_called_while_preventing_writes
+ @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')")
+
+ @conn.while_preventing_writes do
+ assert_equal 1, @conn.execute("SELECT `engines`.* FROM `engines` WHERE `engines`.`car_id` = '138853948594'").entries.count
+ end
+ end
+
+ def test_doesnt_error_when_a_show_query_is_called_while_preventing_writes
+ @conn.while_preventing_writes do
+ assert_equal 2, @conn.execute("SHOW FULL FIELDS FROM `engines`").entries.count
+ end
+ end
+
+ def test_doesnt_error_when_a_set_query_is_called_while_preventing_writes
+ @conn.while_preventing_writes do
+ assert_nil @conn.execute("SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci")
+ end
+ end
+
+ def test_doesnt_error_when_a_read_query_with_leading_chars_is_called_while_preventing_writes
+ @conn.execute("INSERT INTO `engines` (`car_id`) VALUES ('138853948594')")
+
+ @conn.while_preventing_writes do
+ assert_equal 1, @conn.execute("(\n( SELECT `engines`.* FROM `engines` WHERE `engines`.`car_id` = '138853948594' ) )").entries.count
+ end
end
private
diff --git a/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb b/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb
new file mode 100644
index 0000000000..628802b216
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/optimizer_hints_test.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+if supports_optimizer_hints?
+ class Mysql2OptimzerHintsTest < ActiveRecord::Mysql2TestCase
+ fixtures :posts
+
+ def test_optimizer_hints
+ assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
+ posts = Post.optimizer_hints("NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id)")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_includes posts.explain, "| index | index_posts_on_author_id | index_posts_on_author_id |"
+ end
+ end
+
+ def test_optimizer_hints_with_count_subquery
+ assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
+ posts = Post.optimizer_hints("NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id)")
+ posts = posts.select(:id).where(author_id: [0, 1]).limit(5)
+ assert_equal 5, posts.count
+ end
+ end
+
+ def test_optimizer_hints_is_sanitized
+ assert_sql(%r{\ASELECT /\*\+ NO_RANGE_OPTIMIZATION\(posts index_posts_on_author_id\) \*/}) do
+ posts = Post.optimizer_hints("/*+ NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id) */")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_includes posts.explain, "| index | index_posts_on_author_id | index_posts_on_author_id |"
+ end
+
+ assert_sql(%r{\ASELECT /\*\+ `posts`\.\*, \*/}) do
+ posts = Post.optimizer_hints("**// `posts`.*, //**")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_equal({ "id" => 1 }, posts.first.as_json)
+ end
+ end
+
+ def test_optimizer_hints_with_unscope
+ assert_sql(%r{\ASELECT `posts`\.`id`}) do
+ posts = Post.optimizer_hints("/*+ NO_RANGE_OPTIMIZATION(posts index_posts_on_author_id) */")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ posts.unscope(:optimizer_hints).load
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb
index b587e756cf..b8f51acba0 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb
@@ -41,7 +41,7 @@ module ActiveRecord
column_24 = @connection.columns(:mysql_doubles).find { |c| c.name == "float_24" }
column_25 = @connection.columns(:mysql_doubles).find { |c| c.name == "float_25" }
- # Mysql floats are precision 0..24, Mysql doubles are precision 25..53
+ # MySQL floats are precision 0..24, MySQL doubles are precision 25..53
assert_equal 24, column_no_limit.limit
assert_equal 24, column_short.limit
assert_equal 53, column_long.limit
@@ -67,7 +67,7 @@ module ActiveRecord
end
def test_data_source_exists_wrong_schema
- assert(!@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist")
+ assert_not(@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist")
end
def test_dump_indexes
diff --git a/activerecord/test/cases/adapters/mysql2/sp_test.rb b/activerecord/test/cases/adapters/mysql2/sp_test.rb
index 7b6dce71e9..626ef59570 100644
--- a/activerecord/test/cases/adapters/mysql2/sp_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/sp_test.rb
@@ -9,7 +9,7 @@ class Mysql2StoredProcedureTest < ActiveRecord::Mysql2TestCase
def setup
@connection = ActiveRecord::Base.connection
- unless ActiveRecord::Base.connection.version >= "5.6.0"
+ unless ActiveRecord::Base.connection.database_version >= "5.6.0"
skip("no stored procedure support")
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb
index ffde8ed4d8..8494acee3b 100644
--- a/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb
@@ -18,6 +18,7 @@ if ActiveRecord::Base.connection.supports_virtual_columns?
t.string :name
t.virtual :upper_name, type: :string, as: "UPPER(`name`)"
t.virtual :name_length, type: :integer, as: "LENGTH(`name`)", stored: true
+ t.virtual :name_octet_length, type: :integer, as: "OCTET_LENGTH(`name`)", stored: true
end
VirtualColumn.create(name: "Rails")
end
@@ -55,7 +56,8 @@ if ActiveRecord::Base.connection.supports_virtual_columns?
def test_schema_dumping
output = dump_table_schema("virtual_columns")
assert_match(/t\.virtual\s+"upper_name",\s+type: :string,\s+as: "(?:UPPER|UCASE)\(`name`\)"$/i, output)
- assert_match(/t\.virtual\s+"name_length",\s+type: :integer,\s+as: "LENGTH\(`name`\)",\s+stored: true$/i, output)
+ assert_match(/t\.virtual\s+"name_length",\s+type: :integer,\s+as: "(?:octet_length|length)\(`name`\)",\s+stored: true$/i, output)
+ assert_match(/t\.virtual\s+"name_octet_length",\s+type: :integer,\s+as: "(?:octet_length|length)\(`name`\)",\s+stored: true$/i, output)
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
index 308ad1d854..62efaf3bfe 100644
--- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
@@ -4,6 +4,8 @@ require "cases/helper"
class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
def setup
+ ActiveRecord::Base.connection.materialize_transactions
+
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do
def execute(sql, name = nil) sql end
end
@@ -27,7 +29,7 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
def test_add_index
# add_index calls index_name_exists? which can't work since execute is stubbed
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) { |*| false }
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.define_method(:index_name_exists?) { |*| false }
expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" ("last_name") WHERE state = 'active')
assert_equal expected, add_index(:people, :last_name, unique: true, where: "state = 'active'")
@@ -72,12 +74,12 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
add_index(:people, :last_name, algorithm: :copy)
end
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_exists?
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.remove_method :index_name_exists?
end
def test_remove_index
# remove_index calls index_name_for_remove which can't work since execute is stubbed
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_for_remove) do |*|
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.define_method(:index_name_for_remove) do |*|
"index_people_on_last_name"
end
@@ -88,7 +90,7 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
add_index(:people, :last_name, algorithm: :copy)
end
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_for_remove
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.remove_method :index_name_for_remove
end
def test_remove_index_when_name_is_specified
diff --git a/activerecord/test/cases/adapters/postgresql/annotate_test.rb b/activerecord/test/cases/adapters/postgresql/annotate_test.rb
new file mode 100644
index 0000000000..42a2861511
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/annotate_test.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+class PostgresqlAnnotateTest < ActiveRecord::PostgreSQLTestCase
+ fixtures :posts
+
+ def test_annotate_wraps_content_in_an_inline_comment
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
+ posts = Post.select(:id).annotate("foo")
+ assert posts.first
+ end
+ end
+
+ def test_annotate_is_sanitized
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
+ posts = Post.select(:id).annotate("*/foo/*")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
+ posts = Post.select(:id).annotate("**//foo//**")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/ /\* bar \*/}) do
+ posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* \+ MAX_EXECUTION_TIME\(1\) \*/}) do
+ posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)")
+ assert posts.first
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index 58fa7532a2..2e7a4b498f 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -17,7 +17,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
enable_extension!("hstore", @connection)
@connection.transaction do
- @connection.create_table("pg_arrays") do |t|
+ @connection.create_table "pg_arrays", force: true do |t|
t.string "tags", array: true, limit: 255
t.integer "ratings", array: true
t.datetime :datetimes, array: true
@@ -112,6 +112,18 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
assert_predicate column, :array?
end
+ def test_change_column_from_non_array_to_array
+ @connection.add_column :pg_arrays, :snippets, :string
+ @connection.change_column :pg_arrays, :snippets, :text, array: true, default: [], using: "string_to_array(\"snippets\", ',')"
+
+ PgArray.reset_column_information
+ column = PgArray.columns_hash["snippets"]
+
+ assert_equal :text, column.type
+ assert_equal [], PgArray.column_defaults["snippets"]
+ assert_predicate column, :array?
+ end
+
def test_change_column_cant_make_non_array_column_to_array
@connection.add_column :pg_arrays, :a_string, :string
assert_raises ActiveRecord::StatementInvalid do
@@ -226,14 +238,6 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
assert_equal(PgArray.last.tags, tag_values)
end
- def test_insert_fixtures
- tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"]
- assert_deprecated do
- @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays")
- end
- assert_equal(PgArray.last.tags, tag_values)
- end
-
def test_attribute_for_inspect_for_array_field
record = PgArray.new { |a| a.ratings = (1..10).to_a }
assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]", record.attribute_for_inspect(:ratings))
@@ -353,7 +357,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
assert e1.persisted?, "Saving e1"
e2 = klass.create("tags" => ["black", "blue"])
- assert !e2.persisted?, "e2 shouldn't be valid"
+ assert_not e2.persisted?, "e2 shouldn't be valid"
assert e2.errors[:tags].any?, "Should have errors for tags"
assert_equal ["has already been taken"], e2.errors[:tags], "Should have uniqueness message for tags"
end
diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
index 64bb6906cd..531e6b2328 100644
--- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -35,7 +35,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
def test_binary_columns_are_limitless_the_upper_limit_is_one_GB
assert_equal "bytea", @connection.type_to_sql(:binary, limit: 100_000)
- assert_raise ActiveRecord::ActiveRecordError do
+ assert_raise ArgumentError do
@connection.type_to_sql(:binary, limit: 4294967295)
end
end
@@ -49,7 +49,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
end
def test_type_cast_binary_value
- data = "\u001F\x8B".dup.force_encoding("BINARY")
+ data = (+"\u001F\x8B").force_encoding("BINARY")
assert_equal(data, @type.deserialize(data))
end
diff --git a/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb
index 305e033642..79e9efcf06 100644
--- a/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb
@@ -7,22 +7,21 @@ class PostgresqlCaseInsensitiveTest < ActiveRecord::PostgreSQLTestCase
def test_case_insensitiveness
connection = ActiveRecord::Base.connection
- table = Default.arel_table
- column = Default.columns_hash["char1"]
- comparison = connection.case_insensitive_comparison table, :char1, column, nil
+ attr = Default.arel_attribute(:char1)
+ comparison = connection.case_insensitive_comparison(attr, nil)
assert_match(/lower/i, comparison.to_sql)
- column = Default.columns_hash["char2"]
- comparison = connection.case_insensitive_comparison table, :char2, column, nil
+ attr = Default.arel_attribute(:char2)
+ comparison = connection.case_insensitive_comparison(attr, nil)
assert_match(/lower/i, comparison.to_sql)
- column = Default.columns_hash["char3"]
- comparison = connection.case_insensitive_comparison table, :char3, column, nil
+ attr = Default.arel_attribute(:char3)
+ comparison = connection.case_insensitive_comparison(attr, nil)
assert_match(/lower/i, comparison.to_sql)
- column = Default.columns_hash["multiline_default"]
- comparison = connection.case_insensitive_comparison table, :multiline_default, column, nil
+ attr = Default.arel_attribute(:multiline_default)
+ comparison = connection.case_insensitive_comparison(attr, nil)
assert_match(/lower/i, comparison.to_sql)
end
end
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 d1b3c434e1..210758f462 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -15,8 +15,9 @@ module ActiveRecord
def setup
super
@subscriber = SQLSubscriber.new
- @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber)
@connection = ActiveRecord::Base.connection
+ @connection.materialize_transactions
+ @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber)
end
def teardown
@@ -24,14 +25,6 @@ module ActiveRecord
super
end
- def test_truncate
- count = ActiveRecord::Base.connection.execute("select count(*) from comments").first["count"].to_i
- assert_operator count, :>, 0
- ActiveRecord::Base.connection.truncate("comments")
- count = ActiveRecord::Base.connection.execute("select count(*) from comments").first["count"].to_i
- assert_equal 0, count
- end
-
def test_encoding
assert_queries(1) do
assert_not_nil @connection.encoding
@@ -145,34 +138,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 OS X, 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!
@@ -229,7 +203,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
@@ -260,6 +234,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/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
index b7535d5c9a..562cf1f2d1 100644
--- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
@@ -64,7 +64,7 @@ class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase
def test_text_columns_are_limitless_the_upper_limit_is_one_GB
assert_equal "text", @connection.type_to_sql(:text, limit: 100_000)
- assert_raise ActiveRecord::ActiveRecordError do
+ assert_raise ArgumentError do
@connection.type_to_sql(:text, limit: 4294967295)
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/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
index df97ab11e7..0fd7b2c6ed 100644
--- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
@@ -22,23 +22,26 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase
@connection = ActiveRecord::Base.connection
- @old_schema_migration_table_name = ActiveRecord::SchemaMigration.table_name
@old_table_name_prefix = ActiveRecord::Base.table_name_prefix
@old_table_name_suffix = ActiveRecord::Base.table_name_suffix
ActiveRecord::Base.table_name_prefix = "p_"
ActiveRecord::Base.table_name_suffix = "_s"
+ ActiveRecord::SchemaMigration.reset_table_name
+ ActiveRecord::InternalMetadata.reset_table_name
+
ActiveRecord::SchemaMigration.delete_all rescue nil
- ActiveRecord::SchemaMigration.table_name = "p_schema_migrations_s"
ActiveRecord::Migration.verbose = false
end
def teardown
- ActiveRecord::Base.table_name_prefix = @old_table_name_prefix
- ActiveRecord::Base.table_name_suffix = @old_table_name_suffix
ActiveRecord::SchemaMigration.delete_all rescue nil
ActiveRecord::Migration.verbose = true
- ActiveRecord::SchemaMigration.table_name = @old_schema_migration_table_name
+
+ ActiveRecord::Base.table_name_prefix = @old_table_name_prefix
+ ActiveRecord::Base.table_name_suffix = @old_table_name_suffix
+ ActiveRecord::SchemaMigration.reset_table_name
+ ActiveRecord::InternalMetadata.reset_table_name
super
end
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/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
index 8c6f046553..14c262f4ce 100644
--- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
@@ -247,7 +247,7 @@ class PostgreSQLGeometricLineTest < ActiveRecord::PostgreSQLTestCase
class PostgresqlLine < ActiveRecord::Base; end
setup do
- unless ActiveRecord::Base.connection.send(:postgresql_version) >= 90400
+ unless ActiveRecord::Base.connection.database_version >= 90400
skip("line type is not fully implemented")
end
@connection = ActiveRecord::Base.connection
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index 4b061a9375..671d8211a7 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -2,6 +2,7 @@
require "cases/helper"
require "support/schema_dumping_helper"
+require "support/stubs/strong_parameters"
class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
@@ -11,12 +12,6 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase
store_accessor :settings, :language, :timezone
end
- class FakeParameters
- def to_unsafe_h
- { "hi" => "hi" }
- end
- end
-
def setup
@connection = ActiveRecord::Base.connection
@@ -158,6 +153,22 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase
assert_equal "GMT", y.timezone
end
+ def test_changes_with_store_accessors
+ x = Hstore.new(language: "de")
+ assert x.language_changed?
+ assert_nil x.language_was
+ assert_equal [nil, "de"], x.language_change
+ x.save!
+
+ assert_not x.language_changed?
+ x.reload
+
+ x.settings = nil
+ assert x.language_changed?
+ assert_equal "de", x.language_was
+ assert_equal ["de", nil], x.language_change
+ end
+
def test_changes_in_place
hstore = Hstore.create!(settings: { "one" => "two" })
hstore.settings["three"] = "four"
@@ -344,7 +355,7 @@ class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase
end
def test_supports_to_unsafe_h_values
- assert_equal("\"hi\"=>\"hi\"", @type.serialize(FakeParameters.new))
+ assert_equal "\"hi\"=>\"hi\"", @type.serialize(ProtectedParams.new("hi" => "hi"))
end
private
diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
index 5e56ce8427..b1bf06d9e9 100644
--- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
@@ -71,17 +71,15 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
end
test "assigning 'infinity' on a datetime column with TZ aware attributes" do
- begin
- in_time_zone "Pacific Time (US & Canada)" do
- record = PostgresqlInfinity.create!(datetime: "infinity")
- assert_equal Float::INFINITY, record.datetime
- assert_equal record.datetime, record.reload.datetime
- end
- ensure
- # setting time_zone_aware_attributes causes the types to change.
- # There is no way to do this automatically since it can be set on a superclass
- PostgresqlInfinity.reset_column_information
+ in_time_zone "Pacific Time (US & Canada)" do
+ record = PostgresqlInfinity.create!(datetime: "infinity")
+ assert_equal Float::INFINITY, record.datetime
+ assert_equal record.datetime, record.reload.datetime
end
+ ensure
+ # setting time_zone_aware_attributes causes the types to change.
+ # There is no way to do this automatically since it can be set on a superclass
+ PostgresqlInfinity.reset_column_information
end
test "where clause with infinite range on a datetime column" do
diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb
index be3590e8dd..1aa0348879 100644
--- a/activerecord/test/cases/adapters/postgresql/money_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/money_test.rb
@@ -6,7 +6,9 @@ require "support/schema_dumping_helper"
class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase
include SchemaDumpingHelper
- class PostgresqlMoney < ActiveRecord::Base; end
+ class PostgresqlMoney < ActiveRecord::Base
+ validates :depth, numericality: true
+ end
setup do
@connection = ActiveRecord::Base.connection
@@ -35,6 +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
end
def test_money_values
@@ -49,10 +52,10 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase
def test_money_type_cast
type = PostgresqlMoney.type_for_attribute("wealth")
- assert_equal(12345678.12, type.cast("$12,345,678.12".dup))
- assert_equal(12345678.12, type.cast("$12.345.678,12".dup))
- assert_equal(-1.15, type.cast("-$1.15".dup))
- assert_equal(-2.25, type.cast("($2.25)".dup))
+ assert_equal(12345678.12, type.cast(+"$12,345,678.12"))
+ assert_equal(12345678.12, type.cast(+"$12.345.678,12"))
+ assert_equal(-1.15, type.cast(+"-$1.15"))
+ assert_equal(-2.25, type.cast(+"($2.25)"))
end
def test_schema_dumping
@@ -62,7 +65,7 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase
end
def test_create_and_update_money
- money = PostgresqlMoney.create(wealth: "987.65".dup)
+ money = PostgresqlMoney.create(wealth: +"987.65")
assert_equal 987.65, money.wealth
new_value = BigDecimal("123.45")
diff --git a/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb b/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb
new file mode 100644
index 0000000000..5b9f5e0832
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/optimizer_hints_test.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+if supports_optimizer_hints?
+ class PostgresqlOptimzerHintsTest < ActiveRecord::PostgreSQLTestCase
+ fixtures :posts
+
+ def setup
+ enable_extension!("pg_hint_plan", ActiveRecord::Base.connection)
+ end
+
+ def test_optimizer_hints
+ assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
+ posts = Post.optimizer_hints("SeqScan(posts)")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_includes posts.explain, "Seq Scan on posts"
+ end
+ end
+
+ def test_optimizer_hints_with_count_subquery
+ assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
+ posts = Post.optimizer_hints("SeqScan(posts)")
+ posts = posts.select(:id).where(author_id: [0, 1]).limit(5)
+ assert_equal 5, posts.count
+ end
+ end
+
+ def test_optimizer_hints_is_sanitized
+ assert_sql(%r{\ASELECT /\*\+ SeqScan\(posts\) \*/}) do
+ posts = Post.optimizer_hints("/*+ SeqScan(posts) */")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_includes posts.explain, "Seq Scan on posts"
+ end
+
+ assert_sql(%r{\ASELECT /\*\+ "posts"\.\*, \*/}) do
+ posts = Post.optimizer_hints("**// \"posts\".*, //**")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ assert_equal({ "id" => 1 }, posts.first.as_json)
+ end
+ end
+
+ def test_optimizer_hints_with_unscope
+ assert_sql(%r{\ASELECT "posts"\."id"}) do
+ posts = Post.optimizer_hints("/*+ SeqScan(posts) */")
+ posts = posts.select(:id).where(author_id: [0, 1])
+ posts.unscope(:optimizer_hints).load
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/partitions_test.rb b/activerecord/test/cases/adapters/postgresql/partitions_test.rb
new file mode 100644
index 0000000000..4015bc94f9
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/partitions_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+class PostgreSQLPartitionsTest < ActiveRecord::PostgreSQLTestCase
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ @connection.drop_table "partitioned_events", if_exists: true
+ end
+
+ def test_partitions_table_exists
+ skip unless ActiveRecord::Base.connection.database_version >= 100000
+ @connection.create_table :partitioned_events, force: true, id: false,
+ options: "partition by range (issued_at)" do |t|
+ t.timestamp :issued_at
+ end
+ assert @connection.table_exists?("partitioned_events")
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index cbb6cd42b5..fbd3cbf90f 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -376,6 +376,72 @@ 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
+
+ def test_doesnt_error_when_a_read_query_with_leading_chars_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("(\n( SELECT * FROM ex WHERE data = '138853948594' ) )").entries.count
+ end
+ 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 261c24634e..068f1e8bea 100644
--- a/activerecord/test/cases/adapters/postgresql/range_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/range_test.rb
@@ -3,412 +3,432 @@
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
+class PostgresqlRange < ActiveRecord::Base
+ self.table_name = "postgresql_ranges"
+ self.time_zone_aware_types += [:tsrange, :tstzrange]
+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 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
- teardown do
- @connection.drop_table "postgresql_ranges", if_exists: true
- @connection.execute "DROP TYPE IF EXISTS floatrange"
- reset_connection
- 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_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.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_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
+ teardown do
+ @connection.drop_table "postgresql_ranges", if_exists: true
+ @connection.execute "DROP TYPE IF EXISTS floatrange"
+ reset_connection
+ 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_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_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_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_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_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_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_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_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_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_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_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_timezone_awareness_tzrange
- tz = "Pacific Time (US & Canada)"
+ 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
- in_time_zone tz do
- PostgresqlRange.reset_column_information
- time_string = Time.current.to_s
- time = Time.zone.parse(time_string)
+ 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 = 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_timezone_awareness_tzrange
+ tz = "Pacific Time (US & Canada)"
- record.save!
- record.reload
+ in_time_zone tz do
+ PostgresqlRange.reset_column_information
+ time_string = Time.current.to_s
+ time = Time.zone.parse(time_string)
- assert_equal time..time, record.tstz_range
- assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone
- end
- 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_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.save!
+ record.reload
- 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"))
+ assert_equal time..time, record.tstz_range
+ assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone
end
+ end
- 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
+ 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_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_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
- def test_timezone_awareness_tsrange
- tz = "Pacific Time (US & Canada)"
+ 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
- in_time_zone tz do
- PostgresqlRange.reset_column_information
- time_string = Time.current.to_s
- time = Time.zone.parse(time_string)
+ 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 = 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_timezone_awareness_tsrange
+ tz = "Pacific Time (US & Canada)"
- record.save!
- record.reload
+ in_time_zone tz do
+ PostgresqlRange.reset_column_information
+ time_string = Time.current.to_s
+ time = Time.zone.parse(time_string)
- assert_equal time..time, record.ts_range
- assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone
- end
- 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_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.save!
+ record.reload
- 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"))
+ assert_equal time..time, record.ts_range
+ assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone
end
+ end
- 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
+ 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_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_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
- def test_timezone_awareness_tsrange_preserve_usec
- tz = "Pacific Time (US & Canada)"
+ 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
- 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_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 = 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_timezone_awareness_tsrange_preserve_usec
+ tz = "Pacific Time (US & Canada)"
- record.save!
- record.reload
+ 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
- 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
+ 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_create_numrange
- assert_equal_round_trip(@new_range, :num_range,
- BigDecimal("0.5")...BigDecimal("1"))
- end
+ record.save!
+ record.reload
- 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"))
+ 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_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_numrange
+ assert_equal_round_trip(@new_range, :num_range,
+ BigDecimal("0.5")...BigDecimal("1"))
+ 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_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_create_int4range
- assert_equal_round_trip(@new_range, :int4_range, Range.new(3, 50, true))
- 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_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_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_int8range
- assert_equal_round_trip(@new_range, :int8_range, Range.new(30, 50, true))
- end
+ def test_create_int4range
+ assert_equal_round_trip(@new_range, :int4_range, Range.new(3, 50, true))
+ 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_int4range
+ assert_equal_round_trip(@first_range, :int4_range, 6...10)
+ assert_nil_round_trip(@first_range, :int4_range, 3...3)
+ 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_create_int8range
+ assert_equal_round_trip(@new_range, :int8_range, Range.new(30, 50, true))
+ 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_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 test_infinity_values
+ PostgresqlRange.create!(int4_range: 1..Float::INFINITY,
+ int8_range: -Float::INFINITY..0,
+ float_range: -Float::INFINITY..Float::INFINITY)
- def assert_nil_round_trip(range, attribute, value)
- round_trip(range, attribute, value)
- assert_nil range.public_send(attribute)
- end
+ record = PostgresqlRange.first
- def round_trip(range, attribute, value)
- range.public_send "#{attribute}=", value
- assert range.save
- assert range.reload
- 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
- 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
+ if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.6.0")
+ def test_endless_range_values
+ record = PostgresqlRange.create!(
+ int4_range: eval("1.."),
+ int8_range: eval("10.."),
+ float_range: eval("0.5..")
+ )
+
+ record = PostgresqlRange.find(record.id)
+
+ assert_equal 1...Float::INFINITY, record.int4_range
+ assert_equal 10...Float::INFINITY, record.int8_range
+ assert_equal 0.5...Float::INFINITY, record.float_range
+ 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 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/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb
index 0bcc214c24..ba477c63f4 100644
--- a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb
@@ -101,7 +101,7 @@ class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase
@connection.extend ProgrammerMistake
assert_raises ArgumentError do
- @connection.disable_referential_integrity {}
+ @connection.disable_referential_integrity { }
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 7ad03c194f..336cec30ca 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -108,23 +108,19 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
end
def test_create_schema
- begin
- @connection.create_schema "test_schema3"
- assert @connection.schema_names.include? "test_schema3"
- ensure
- @connection.drop_schema "test_schema3"
- end
+ @connection.create_schema "test_schema3"
+ assert @connection.schema_names.include? "test_schema3"
+ ensure
+ @connection.drop_schema "test_schema3"
end
def test_raise_create_schema_with_existing_schema
- begin
+ @connection.create_schema "test_schema3"
+ assert_raises(ActiveRecord::StatementInvalid) do
@connection.create_schema "test_schema3"
- assert_raises(ActiveRecord::StatementInvalid) do
- @connection.create_schema "test_schema3"
- end
- ensure
- @connection.drop_schema "test_schema3"
end
+ ensure
+ @connection.drop_schema "test_schema3"
end
def test_drop_schema
@@ -146,7 +142,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);
@@ -204,12 +200,12 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
def test_data_source_exists_when_not_on_schema_search_path
with_schema_search_path("PUBLIC") do
- assert(!@connection.data_source_exists?(TABLE_NAME), "data_source exists but should not be found")
+ assert_not(@connection.data_source_exists?(TABLE_NAME), "data_source exists but should not be found")
end
end
def test_data_source_exists_wrong_schema
- assert(!@connection.data_source_exists?("foo.things"), "data_source should not exist")
+ assert_not(@connection.data_source_exists?("foo.things"), "data_source should not exist")
end
def test_data_source_exists_quoted_names
@@ -507,6 +503,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 +527,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..d2d8ea8042 100644
--- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
@@ -114,6 +114,22 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
assert_equal "foobar", uuid.guid_before_type_cast
end
+ def test_invalid_uuid_dont_match_to_nil
+ UUIDType.create!
+ assert_empty UUIDType.where(guid: "")
+ assert_empty UUIDType.where(guid: "foobar")
+ end
+
+ class DuckUUID
+ def initialize(uuid)
+ @uuid = uuid
+ end
+
+ def to_s
+ @uuid
+ end
+ end
+
def test_acceptable_uuid_regex
# Valid uuids
["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11",
@@ -125,9 +141,11 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
# so we shouldn't block it either. (Pay attention to "fb6d" – the "f" here
# is invalid – it must be one of 8, 9, A, B, a, b according to the spec.)
"{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}",
+ # Support Object-Oriented UUIDs which respond to #to_s
+ DuckUUID.new("A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11"),
].each do |valid_uuid|
uuid = UUIDType.new guid: valid_uuid
- assert_not_nil uuid.guid
+ assert_instance_of String, uuid.guid
end
# Invalid uuids
@@ -198,10 +216,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/annotate_test.rb b/activerecord/test/cases/adapters/sqlite3/annotate_test.rb
new file mode 100644
index 0000000000..6567a5eca3
--- /dev/null
+++ b/activerecord/test/cases/adapters/sqlite3/annotate_test.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+class SQLite3AnnotateTest < ActiveRecord::SQLite3TestCase
+ fixtures :posts
+
+ def test_annotate_wraps_content_in_an_inline_comment
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
+ posts = Post.select(:id).annotate("foo")
+ assert posts.first
+ end
+ end
+
+ def test_annotate_is_sanitized
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
+ posts = Post.select(:id).annotate("*/foo/*")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/}) do
+ posts = Post.select(:id).annotate("**//foo//**")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* foo \*/ /\* bar \*/}) do
+ posts = Post.select(:id).annotate("*/foo/*").annotate("*/bar")
+ assert posts.first
+ end
+
+ assert_sql(%r{\ASELECT "posts"\."id" FROM "posts" /\* \+ MAX_EXECUTION_TIME\(1\) \*/}) do
+ posts = Post.select(:id).annotate("+ MAX_EXECUTION_TIME(1)")
+ assert posts.first
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/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/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index 1c85ff5674..9d26f32102 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -6,12 +6,8 @@ require "securerandom"
class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase
def setup
+ super
@conn = ActiveRecord::Base.connection
- @initial_represent_boolean_as_integer = ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer
- end
-
- def teardown
- ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = @initial_represent_boolean_as_integer
end
def test_type_cast_binary_encoding_without_logger
@@ -22,18 +18,10 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase
end
def test_type_cast_true
- ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = false
- assert_equal "t", @conn.type_cast(true)
-
- ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true
assert_equal 1, @conn.type_cast(true)
end
def test_type_cast_false
- ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = false
- assert_equal "f", @conn.type_cast(false)
-
- ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer = true
assert_equal 0, @conn.type_cast(false)
end
@@ -62,4 +50,30 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase
assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value))
end
+
+ def test_quoted_time_dst_utc
+ with_env_tz "America/New_York" do
+ with_timezone_config default: :utc do
+ t = Time.new(2000, 7, 1, 0, 0, 0, "+04:30")
+
+ expected = t.change(year: 2000, month: 1, day: 1)
+ expected = expected.getutc.to_s(:db).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ")
+
+ assert_equal expected, @conn.quoted_time(t)
+ end
+ end
+ end
+
+ def test_quoted_time_dst_local
+ with_env_tz "America/New_York" do
+ with_timezone_config default: :local do
+ t = Time.new(2000, 7, 1, 0, 0, 0, "+04:30")
+
+ expected = t.change(year: 2000, month: 1, day: 1)
+ expected = expected.getlocal.to_s(:db).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ")
+
+ assert_equal expected, @conn.quoted_time(t)
+ 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 7e0ce3a28e..806cfbfc00 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -55,13 +55,13 @@ 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(!result.rows.first.include?("blob"), "should not store blobs")
+ assert_not(result.rows.first.include?("blob"), "should not store blobs")
ensure
owner.delete
end
@@ -87,7 +87,7 @@ module ActiveRecord
def test_connection_no_db
assert_raises(ArgumentError) do
- Base.sqlite3_connection {}
+ Base.sqlite3_connection { }
end
end
@@ -160,14 +160,14 @@ 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
- str = "\x80".dup.force_encoding("ASCII-8BIT")
+ SQL
+ str = (+"\x80").force_encoding("ASCII-8BIT")
binary = DualEncoding.new name: "いただきます!", data: str
binary.save!
assert_equal str, binary.data
@@ -176,7 +176,7 @@ module ActiveRecord
end
def test_type_cast_should_not_mutate_encoding
- name = "hello".dup.force_encoding(Encoding::ASCII_8BIT)
+ name = (+"hello").force_encoding(Encoding::ASCII_8BIT)
Owner.create(name: name)
assert_equal Encoding::ASCII_8BIT, name.encoding
ensure
@@ -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
@@ -345,6 +345,42 @@ module ActiveRecord
end
end
+ if ActiveRecord::Base.connection.supports_expression_index?
+ def test_expression_index
+ with_example_table do
+ @conn.add_index "ex", "max(id, number)", name: "expression"
+ index = @conn.indexes("ex").find { |idx| idx.name == "expression" }
+ assert_equal "max(id, number)", index.columns
+ end
+ end
+
+ def test_expression_index_with_where
+ with_example_table do
+ @conn.add_index "ex", "id % 10, max(id, number)", name: "expression", where: "id > 1000"
+ index = @conn.indexes("ex").find { |idx| idx.name == "expression" }
+ assert_equal "id % 10, max(id, number)", index.columns
+ assert_equal "id > 1000", index.where
+ end
+ end
+
+ def test_complicated_expression
+ with_example_table do
+ @conn.execute "CREATE INDEX expression ON ex (id % 10, (CASE WHEN number > 0 THEN max(id, number) END))WHERE(id > 1000)"
+ index = @conn.indexes("ex").find { |idx| idx.name == "expression" }
+ assert_equal "id % 10, (CASE WHEN number > 0 THEN max(id, number) END)", index.columns
+ assert_equal "(id > 1000)", index.where
+ end
+ end
+
+ def test_not_everything_an_expression
+ with_example_table do
+ @conn.add_index "ex", "id, max(id, number)", name: "expression"
+ index = @conn.indexes("ex").find { |idx| idx.name == "expression" }
+ assert_equal "id, max(id, number)", index.columns
+ end
+ end
+ end
+
def test_primary_key
with_example_table do
assert_equal "id", @conn.primary_key("ex")
@@ -500,8 +536,103 @@ module ActiveRecord
end
end
- def test_deprecate_valid_alter_table_type
- assert_deprecated { @conn.valid_alter_table_type?(:string) }
+ def test_db_is_not_readonly_when_readonly_option_is_false
+ conn = Base.sqlite3_connection database: ":memory:",
+ adapter: "sqlite3",
+ readonly: false
+
+ assert_not_predicate conn.raw_connection, :readonly?
+ end
+
+ def test_db_is_not_readonly_when_readonly_option_is_unspecified
+ conn = Base.sqlite3_connection database: ":memory:",
+ adapter: "sqlite3"
+
+ assert_not_predicate conn.raw_connection, :readonly?
+ end
+
+ def test_db_is_readonly_when_readonly_option_is_true
+ conn = Base.sqlite3_connection database: ":memory:",
+ adapter: "sqlite3",
+ readonly: true
+
+ assert_predicate conn.raw_connection, :readonly?
+ end
+
+ def test_writes_are_not_permitted_to_readonly_databases
+ conn = Base.sqlite3_connection database: ":memory:",
+ adapter: "sqlite3",
+ readonly: true
+
+ assert_raises(ActiveRecord::StatementInvalid, /SQLite3::ReadOnlyException/) do
+ conn.execute("CREATE TABLE test(id integer)")
+ 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
+
+ def test_doesnt_error_when_a_read_query_with_leading_chars_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
@@ -516,7 +647,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/adapters/sqlite3/sqlite3_create_folder_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
index d70486605f..cfc9853aba 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
@@ -8,17 +8,15 @@ module ActiveRecord
class SQLite3CreateFolder < ActiveRecord::SQLite3TestCase
def test_sqlite_creates_directory
Dir.mktmpdir do |dir|
- begin
- dir = Pathname.new(dir)
- @conn = Base.sqlite3_connection database: dir.join("db/foo.sqlite3"),
- adapter: "sqlite3",
- timeout: 100
+ dir = Pathname.new(dir)
+ @conn = Base.sqlite3_connection database: dir.join("db/foo.sqlite3"),
+ adapter: "sqlite3",
+ timeout: 100
- assert Dir.exist? dir.join("db")
- assert File.exist? dir.join("db/foo.sqlite3")
- ensure
- @conn.disconnect! if @conn
- end
+ assert Dir.exist? dir.join("db")
+ assert File.exist? dir.join("db/foo.sqlite3")
+ ensure
+ @conn.disconnect! if @conn
end
end
end
diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb
index fbdf2ada4b..d270175af4 100644
--- a/activerecord/test/cases/aggregations_test.rb
+++ b/activerecord/test/cases/aggregations_test.rb
@@ -27,7 +27,7 @@ class AggregationsTest < ActiveRecord::TestCase
def test_immutable_value_objects
customers(:david).balance = Money.new(100)
- assert_raise(frozen_error_class) { customers(:david).balance.instance_eval { @amount = 20 } }
+ assert_raise(FrozenError) { customers(:david).balance.instance_eval { @amount = 20 } }
end
def test_inferred_mapping
diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb
index 140d7cbcae..9027cc582a 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -51,11 +51,11 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
assert_equal 7, @connection.migration_context.current_version
end
- def test_schema_define_w_table_name_prefix
- table_name = ActiveRecord::SchemaMigration.table_name
+ def test_schema_define_with_table_name_prefix
old_table_name_prefix = ActiveRecord::Base.table_name_prefix
ActiveRecord::Base.table_name_prefix = "nep_"
- ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}"
+ ActiveRecord::SchemaMigration.reset_table_name
+ ActiveRecord::InternalMetadata.reset_table_name
ActiveRecord::Schema.define(version: 7) do
create_table :fruits do |t|
t.column :color, :string
@@ -67,7 +67,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
assert_equal 7, @connection.migration_context.current_version
ensure
ActiveRecord::Base.table_name_prefix = old_table_name_prefix
- ActiveRecord::SchemaMigration.table_name = table_name
+ ActiveRecord::SchemaMigration.reset_table_name
+ ActiveRecord::InternalMetadata.reset_table_name
end
def test_schema_raises_an_error_for_invalid_column_type
@@ -116,8 +117,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
end
end
- assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
- assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
+ assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
end
def test_timestamps_without_null_set_null_to_false_on_change_table
@@ -129,8 +130,23 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
end
end
- assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
- assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
+ assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
+ end
+
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_timestamps_without_null_set_null_to_false_on_change_table_with_bulk
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+
+ change_table :has_timestamps, bulk: true do |t|
+ t.timestamps default: Time.now
+ end
+ end
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
+ end
end
def test_timestamps_without_null_set_null_to_false_on_add_timestamps
@@ -139,7 +155,58 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
add_timestamps :has_timestamps, default: Time.now
end
- assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
- assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
+ assert @connection.column_exists?(:has_timestamps, :created_at, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, null: false)
+ end
+
+ if subsecond_precision_supported?
+ def test_timestamps_sets_presicion_on_create_table
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps do |t|
+ t.timestamps
+ end
+ end
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
+ end
+
+ def test_timestamps_sets_presicion_on_change_table
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+
+ change_table :has_timestamps do |t|
+ t.timestamps default: Time.now
+ end
+ end
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
+ end
+
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_timestamps_sets_presicion_on_change_table_with_bulk
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+
+ change_table :has_timestamps, bulk: true do |t|
+ t.timestamps default: Time.now
+ end
+ end
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
+ end
+ end
+
+ def test_timestamps_sets_presicion_on_add_timestamps
+ ActiveRecord::Schema.define do
+ create_table :has_timestamps
+ add_timestamps :has_timestamps, default: Time.now
+ end
+
+ assert @connection.column_exists?(:has_timestamps, :created_at, precision: 6, null: false)
+ assert @connection.column_exists?(:has_timestamps, :updated_at, precision: 6, null: false)
+ end
end
end
diff --git a/activerecord/test/cases/arel/attributes/attribute_test.rb b/activerecord/test/cases/arel/attributes/attribute_test.rb
new file mode 100644
index 0000000000..c7bd0a053b
--- /dev/null
+++ b/activerecord/test/cases/arel/attributes/attribute_test.rb
@@ -0,0 +1,1080 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "ostruct"
+
+module Arel
+ module Attributes
+ class AttributeTest < Arel::Spec
+ describe "#not_eq" do
+ it "should create a NotEqual node" do
+ relation = Table.new(:users)
+ relation[:id].not_eq(10).must_be_kind_of Nodes::NotEqual
+ end
+
+ it "should generate != in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_eq(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" != 10
+ }
+ end
+
+ it "should handle nil" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_eq(nil)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" IS NOT NULL
+ }
+ end
+ end
+
+ describe "#not_eq_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].not_eq_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_eq_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" != 1 OR "users"."id" != 2)
+ }
+ end
+ end
+
+ describe "#not_eq_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].not_eq_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_eq_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" != 1 AND "users"."id" != 2)
+ }
+ end
+ end
+
+ describe "#gt" do
+ it "should create a GreaterThan node" do
+ relation = Table.new(:users)
+ relation[:id].gt(10).must_be_kind_of Nodes::GreaterThan
+ end
+
+ it "should generate > in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gt(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" > 10
+ }
+ end
+
+ it "should handle comparing with a subquery" do
+ users = Table.new(:users)
+
+ avg = users.project(users[:karma].average)
+ mgr = users.project(Arel.star).where(users[:karma].gt(avg))
+
+ mgr.to_sql.must_be_like %{
+ SELECT * FROM "users" WHERE "users"."karma" > (SELECT AVG("users"."karma") FROM "users")
+ }
+ end
+
+ it "should accept various data types." do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].gt("fake_name")
+ mgr.to_sql.must_match %{"users"."name" > 'fake_name'}
+
+ current_time = ::Time.now
+ mgr.where relation[:created_at].gt(current_time)
+ mgr.to_sql.must_match %{"users"."created_at" > '#{current_time}'}
+ end
+ end
+
+ describe "#gt_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].gt_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gt_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" > 1 OR "users"."id" > 2)
+ }
+ end
+ end
+
+ describe "#gt_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].gt_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gt_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" > 1 AND "users"."id" > 2)
+ }
+ end
+ end
+
+ describe "#gteq" do
+ it "should create a GreaterThanOrEqual node" do
+ relation = Table.new(:users)
+ relation[:id].gteq(10).must_be_kind_of Nodes::GreaterThanOrEqual
+ end
+
+ it "should generate >= in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gteq(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" >= 10
+ }
+ end
+
+ it "should accept various data types." do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].gteq("fake_name")
+ mgr.to_sql.must_match %{"users"."name" >= 'fake_name'}
+
+ current_time = ::Time.now
+ mgr.where relation[:created_at].gteq(current_time)
+ mgr.to_sql.must_match %{"users"."created_at" >= '#{current_time}'}
+ end
+ end
+
+ describe "#gteq_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].gteq_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gteq_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" >= 1 OR "users"."id" >= 2)
+ }
+ end
+ end
+
+ describe "#gteq_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].gteq_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].gteq_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" >= 1 AND "users"."id" >= 2)
+ }
+ end
+ end
+
+ describe "#lt" do
+ it "should create a LessThan node" do
+ relation = Table.new(:users)
+ relation[:id].lt(10).must_be_kind_of Nodes::LessThan
+ end
+
+ it "should generate < in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lt(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" < 10
+ }
+ end
+
+ it "should accept various data types." do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].lt("fake_name")
+ mgr.to_sql.must_match %{"users"."name" < 'fake_name'}
+
+ current_time = ::Time.now
+ mgr.where relation[:created_at].lt(current_time)
+ mgr.to_sql.must_match %{"users"."created_at" < '#{current_time}'}
+ end
+ end
+
+ describe "#lt_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].lt_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lt_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" < 1 OR "users"."id" < 2)
+ }
+ end
+ end
+
+ describe "#lt_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].lt_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lt_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" < 1 AND "users"."id" < 2)
+ }
+ end
+ end
+
+ describe "#lteq" do
+ it "should create a LessThanOrEqual node" do
+ relation = Table.new(:users)
+ relation[:id].lteq(10).must_be_kind_of Nodes::LessThanOrEqual
+ end
+
+ it "should generate <= in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lteq(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" <= 10
+ }
+ end
+
+ it "should accept various data types." do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].lteq("fake_name")
+ mgr.to_sql.must_match %{"users"."name" <= 'fake_name'}
+
+ current_time = ::Time.now
+ mgr.where relation[:created_at].lteq(current_time)
+ mgr.to_sql.must_match %{"users"."created_at" <= '#{current_time}'}
+ end
+ end
+
+ describe "#lteq_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].lteq_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lteq_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" <= 1 OR "users"."id" <= 2)
+ }
+ end
+ end
+
+ describe "#lteq_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].lteq_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].lteq_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" <= 1 AND "users"."id" <= 2)
+ }
+ end
+ end
+
+ describe "#average" do
+ it "should create a AVG node" do
+ relation = Table.new(:users)
+ relation[:id].average.must_be_kind_of Nodes::Avg
+ end
+
+ it "should generate the proper SQL" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id].average
+ mgr.to_sql.must_be_like %{
+ SELECT AVG("users"."id")
+ FROM "users"
+ }
+ end
+ end
+
+ describe "#maximum" do
+ it "should create a MAX node" do
+ relation = Table.new(:users)
+ relation[:id].maximum.must_be_kind_of Nodes::Max
+ end
+
+ it "should generate proper SQL" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id].maximum
+ mgr.to_sql.must_be_like %{
+ SELECT MAX("users"."id")
+ FROM "users"
+ }
+ end
+ end
+
+ describe "#minimum" do
+ it "should create a Min node" do
+ relation = Table.new(:users)
+ relation[:id].minimum.must_be_kind_of Nodes::Min
+ end
+
+ it "should generate proper SQL" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id].minimum
+ mgr.to_sql.must_be_like %{
+ SELECT MIN("users"."id")
+ FROM "users"
+ }
+ end
+ end
+
+ describe "#sum" do
+ it "should create a SUM node" do
+ relation = Table.new(:users)
+ relation[:id].sum.must_be_kind_of Nodes::Sum
+ end
+
+ it "should generate the proper SQL" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id].sum
+ mgr.to_sql.must_be_like %{
+ SELECT SUM("users"."id")
+ FROM "users"
+ }
+ end
+ end
+
+ describe "#count" do
+ it "should return a count node" do
+ relation = Table.new(:users)
+ relation[:id].count.must_be_kind_of Nodes::Count
+ end
+
+ it "should take a distinct param" do
+ relation = Table.new(:users)
+ count = relation[:id].count(nil)
+ count.must_be_kind_of Nodes::Count
+ count.distinct.must_be_nil
+ end
+ end
+
+ describe "#eq" do
+ it "should return an equality node" do
+ attribute = Attribute.new nil, nil
+ equality = attribute.eq 1
+ equality.left.must_equal attribute
+ equality.right.val.must_equal 1
+ equality.must_be_kind_of Nodes::Equality
+ end
+
+ it "should generate = in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq(10)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" = 10
+ }
+ end
+
+ it "should handle nil" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq(nil)
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" IS NULL
+ }
+ end
+ end
+
+ describe "#eq_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].eq_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq_any([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 OR "users"."id" = 2)
+ }
+ end
+
+ it "should not eat input" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ values = [1, 2]
+ mgr.where relation[:id].eq_any(values)
+ values.must_equal [1, 2]
+ end
+ end
+
+ describe "#eq_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].eq_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 AND "users"."id" = 2)
+ }
+ end
+
+ it "should not eat input" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ values = [1, 2]
+ mgr.where relation[:id].eq_all(values)
+ values.must_equal [1, 2]
+ end
+ end
+
+ describe "#matches" do
+ it "should create a Matches node" do
+ relation = Table.new(:users)
+ relation[:name].matches("%bacon%").must_be_kind_of Nodes::Matches
+ end
+
+ it "should generate LIKE in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].matches("%bacon%")
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."name" LIKE '%bacon%'
+ }
+ end
+ end
+
+ describe "#matches_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:name].matches_any(["%chunky%", "%bacon%"]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].matches_any(["%chunky%", "%bacon%"])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."name" LIKE '%chunky%' OR "users"."name" LIKE '%bacon%')
+ }
+ end
+ end
+
+ describe "#matches_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:name].matches_all(["%chunky%", "%bacon%"]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].matches_all(["%chunky%", "%bacon%"])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."name" LIKE '%chunky%' AND "users"."name" LIKE '%bacon%')
+ }
+ end
+ end
+
+ describe "#does_not_match" do
+ it "should create a DoesNotMatch node" do
+ relation = Table.new(:users)
+ relation[:name].does_not_match("%bacon%").must_be_kind_of Nodes::DoesNotMatch
+ end
+
+ it "should generate NOT LIKE in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].does_not_match("%bacon%")
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."name" NOT LIKE '%bacon%'
+ }
+ end
+ end
+
+ describe "#does_not_match_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:name].does_not_match_any(["%chunky%", "%bacon%"]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].does_not_match_any(["%chunky%", "%bacon%"])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."name" NOT LIKE '%chunky%' OR "users"."name" NOT LIKE '%bacon%')
+ }
+ end
+ end
+
+ describe "#does_not_match_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:name].does_not_match_all(["%chunky%", "%bacon%"]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].does_not_match_all(["%chunky%", "%bacon%"])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."name" NOT LIKE '%chunky%' AND "users"."name" NOT LIKE '%bacon%')
+ }
+ end
+ end
+
+ describe "#between" do
+ it "can be constructed with a standard range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(1..3)
+
+ node.must_equal Nodes::Between.new(
+ attribute,
+ Nodes::And.new([
+ Nodes::Casted.new(1, attribute),
+ Nodes::Casted.new(3, attribute)
+ ])
+ )
+ end
+
+ it "can be constructed with a range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(-::Float::INFINITY..3)
+
+ node.must_equal Nodes::LessThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ end
+
+ it "can be constructed with a quoted range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(quoted_range(-::Float::INFINITY, 3, false))
+
+ node.must_equal Nodes::LessThanOrEqual.new(
+ attribute,
+ Nodes::Quoted.new(3)
+ )
+ end
+
+ it "can be constructed with an exclusive range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(-::Float::INFINITY...3)
+
+ node.must_equal Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ end
+
+ it "can be constructed with a quoted exclusive range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(quoted_range(-::Float::INFINITY, 3, true))
+
+ node.must_equal Nodes::LessThan.new(
+ attribute,
+ Nodes::Quoted.new(3)
+ )
+ end
+
+ it "can be constructed with an infinite range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(-::Float::INFINITY..::Float::INFINITY)
+
+ node.must_equal Nodes::NotIn.new(attribute, [])
+ end
+
+ it "can be constructed with a quoted infinite range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(quoted_range(-::Float::INFINITY, ::Float::INFINITY, false))
+
+ node.must_equal Nodes::NotIn.new(attribute, [])
+ end
+
+ it "can be constructed with a range ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(0..::Float::INFINITY)
+
+ node.must_equal Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ )
+ end
+
+ if Gem::Version.new("2.6.0") <= Gem::Version.new(RUBY_VERSION)
+ it "can be constructed with a range implicitly ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(eval("0..")) # Use eval for compatibility with Ruby < 2.6 parser
+
+ node.must_equal Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ )
+ end
+ end
+
+ it "can be constructed with a quoted range ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(quoted_range(0, ::Float::INFINITY, false))
+
+ node.must_equal Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Quoted.new(0)
+ )
+ end
+
+ it "can be constructed with an exclusive range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.between(0...3)
+
+ node.must_equal Nodes::And.new([
+ Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ ),
+ Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ ])
+ end
+ end
+
+ describe "#in" do
+ it "can be constructed with a subquery" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].does_not_match_all(["%chunky%", "%bacon%"])
+ attribute = Attribute.new nil, nil
+
+ node = attribute.in(mgr)
+
+ node.must_equal Nodes::In.new(attribute, mgr.ast)
+ end
+
+ it "can be constructed with a list" do
+ attribute = Attribute.new nil, nil
+ node = attribute.in([1, 2, 3])
+
+ node.must_equal Nodes::In.new(
+ attribute,
+ [
+ Nodes::Casted.new(1, attribute),
+ Nodes::Casted.new(2, attribute),
+ Nodes::Casted.new(3, attribute),
+ ]
+ )
+ end
+
+ it "can be constructed with a random object" do
+ attribute = Attribute.new nil, nil
+ random_object = Object.new
+ node = attribute.in(random_object)
+
+ node.must_equal Nodes::In.new(
+ attribute,
+ Nodes::Casted.new(random_object, attribute)
+ )
+ end
+
+ it "should generate IN in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].in([1, 2, 3])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" IN (1, 2, 3)
+ }
+ end
+ end
+
+ describe "#in_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].in_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].in_any([[1, 2], [3, 4]])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" IN (1, 2) OR "users"."id" IN (3, 4))
+ }
+ end
+ end
+
+ describe "#in_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].in_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].in_all([[1, 2], [3, 4]])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" IN (1, 2) AND "users"."id" IN (3, 4))
+ }
+ end
+ end
+
+ describe "#not_between" do
+ it "can be constructed with a standard range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(1..3)
+
+ node.must_equal Nodes::Grouping.new(
+ Nodes::Or.new(
+ Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(1, attribute)
+ ),
+ Nodes::GreaterThan.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ )
+ )
+ end
+
+ it "can be constructed with a range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(-::Float::INFINITY..3)
+
+ node.must_equal Nodes::GreaterThan.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ end
+
+ it "can be constructed with a quoted range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(quoted_range(-::Float::INFINITY, 3, false))
+
+ node.must_equal Nodes::GreaterThan.new(
+ attribute,
+ Nodes::Quoted.new(3)
+ )
+ end
+
+ it "can be constructed with an exclusive range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(-::Float::INFINITY...3)
+
+ node.must_equal Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ end
+
+ it "can be constructed with a quoted exclusive range starting from -Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(quoted_range(-::Float::INFINITY, 3, true))
+
+ node.must_equal Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Quoted.new(3)
+ )
+ end
+
+ it "can be constructed with an infinite range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(-::Float::INFINITY..::Float::INFINITY)
+
+ node.must_equal Nodes::In.new(attribute, [])
+ end
+
+ it "can be constructed with a quoted infinite range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(quoted_range(-::Float::INFINITY, ::Float::INFINITY, false))
+
+ node.must_equal Nodes::In.new(attribute, [])
+ end
+
+ it "can be constructed with a range ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(0..::Float::INFINITY)
+
+ node.must_equal Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ )
+ end
+
+ if Gem::Version.new("2.6.0") <= Gem::Version.new(RUBY_VERSION)
+ it "can be constructed with a range implicitly ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(eval("0..")) # Use eval for compatibility with Ruby < 2.6 parser
+
+ node.must_equal Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ )
+ end
+ end
+
+ it "can be constructed with a quoted range ending at Infinity" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(quoted_range(0, ::Float::INFINITY, false))
+
+ node.must_equal Nodes::LessThan.new(
+ attribute,
+ Nodes::Quoted.new(0)
+ )
+ end
+
+ it "can be constructed with an exclusive range" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_between(0...3)
+
+ node.must_equal Nodes::Grouping.new(
+ Nodes::Or.new(
+ Nodes::LessThan.new(
+ attribute,
+ Nodes::Casted.new(0, attribute)
+ ),
+ Nodes::GreaterThanOrEqual.new(
+ attribute,
+ Nodes::Casted.new(3, attribute)
+ )
+ )
+ )
+ end
+ end
+
+ describe "#not_in" do
+ it "can be constructed with a subquery" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:name].does_not_match_all(["%chunky%", "%bacon%"])
+ attribute = Attribute.new nil, nil
+
+ node = attribute.not_in(mgr)
+
+ node.must_equal Nodes::NotIn.new(attribute, mgr.ast)
+ end
+
+ it "can be constructed with a Union" do
+ relation = Table.new(:users)
+ mgr1 = relation.project(relation[:id])
+ mgr2 = relation.project(relation[:id])
+
+ union = mgr1.union(mgr2)
+ node = relation[:id].in(union)
+ node.to_sql.must_be_like %{
+ "users"."id" IN (( SELECT "users"."id" FROM "users" UNION SELECT "users"."id" FROM "users" ))
+ }
+ end
+
+ it "can be constructed with a list" do
+ attribute = Attribute.new nil, nil
+ node = attribute.not_in([1, 2, 3])
+
+ node.must_equal Nodes::NotIn.new(
+ attribute,
+ [
+ Nodes::Casted.new(1, attribute),
+ Nodes::Casted.new(2, attribute),
+ Nodes::Casted.new(3, attribute),
+ ]
+ )
+ end
+
+ it "can be constructed with a random object" do
+ attribute = Attribute.new nil, nil
+ random_object = Object.new
+ node = attribute.not_in(random_object)
+
+ node.must_equal Nodes::NotIn.new(
+ attribute,
+ Nodes::Casted.new(random_object, attribute)
+ )
+ end
+
+ it "should generate NOT IN in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_in([1, 2, 3])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE "users"."id" NOT IN (1, 2, 3)
+ }
+ end
+ end
+
+ describe "#not_in_any" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].not_in_any([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ORs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_in_any([[1, 2], [3, 4]])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" NOT IN (1, 2) OR "users"."id" NOT IN (3, 4))
+ }
+ end
+ end
+
+ describe "#not_in_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].not_in_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].not_in_all([[1, 2], [3, 4]])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" NOT IN (1, 2) AND "users"."id" NOT IN (3, 4))
+ }
+ end
+ end
+
+ describe "#eq_all" do
+ it "should create a Grouping node" do
+ relation = Table.new(:users)
+ relation[:id].eq_all([1, 2]).must_be_kind_of Nodes::Grouping
+ end
+
+ it "should generate ANDs in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.where relation[:id].eq_all([1, 2])
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" WHERE ("users"."id" = 1 AND "users"."id" = 2)
+ }
+ end
+ end
+
+ describe "#asc" do
+ it "should create an Ascending node" do
+ relation = Table.new(:users)
+ relation[:id].asc.must_be_kind_of Nodes::Ascending
+ end
+
+ it "should generate ASC in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.order relation[:id].asc
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" ORDER BY "users"."id" ASC
+ }
+ end
+ end
+
+ describe "#desc" do
+ it "should create a Descending node" do
+ relation = Table.new(:users)
+ relation[:id].desc.must_be_kind_of Nodes::Descending
+ end
+
+ it "should generate DESC in sql" do
+ relation = Table.new(:users)
+ mgr = relation.project relation[:id]
+ mgr.order relation[:id].desc
+ mgr.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" ORDER BY "users"."id" DESC
+ }
+ end
+ end
+
+ describe "equality" do
+ describe "#to_sql" do
+ it "should produce sql" do
+ table = Table.new :users
+ condition = table["id"].eq 1
+ condition.to_sql.must_equal '"users"."id" = 1'
+ end
+ end
+ end
+
+ describe "type casting" do
+ it "does not type cast by default" do
+ table = Table.new(:foo)
+ condition = table["id"].eq("1")
+
+ assert_not table.able_to_type_cast?
+ condition.to_sql.must_equal %("foo"."id" = '1')
+ end
+
+ it "type casts when given an explicit caster" do
+ fake_caster = Object.new
+ def fake_caster.type_cast_for_database(attr_name, value)
+ if attr_name == "id"
+ value.to_i
+ else
+ value
+ end
+ end
+ table = Table.new(:foo, type_caster: fake_caster)
+ condition = table["id"].eq("1").and(table["other_id"].eq("2"))
+
+ assert table.able_to_type_cast?
+ condition.to_sql.must_equal %("foo"."id" = 1 AND "foo"."other_id" = '2')
+ end
+
+ it "does not type cast SqlLiteral nodes" do
+ fake_caster = Object.new
+ def fake_caster.type_cast_for_database(attr_name, value)
+ value.to_i
+ end
+ table = Table.new(:foo, type_caster: fake_caster)
+ condition = table["id"].eq(Arel.sql("(select 1)"))
+
+ assert table.able_to_type_cast?
+ condition.to_sql.must_equal %("foo"."id" = (select 1))
+ end
+ end
+
+ private
+ def quoted_range(begin_val, end_val, exclude)
+ OpenStruct.new(
+ begin: Nodes::Quoted.new(begin_val),
+ end: Nodes::Quoted.new(end_val),
+ exclude_end?: exclude,
+ )
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/attributes/math_test.rb b/activerecord/test/cases/arel/attributes/math_test.rb
new file mode 100644
index 0000000000..41eea217c0
--- /dev/null
+++ b/activerecord/test/cases/arel/attributes/math_test.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Attributes
+ class MathTest < Arel::Spec
+ %i[* /].each do |math_operator|
+ it "average should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].average.public_send(math_operator, 2)).to_sql.must_be_like %{
+ AVG("users"."id") #{math_operator} 2
+ }
+ end
+
+ it "count should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].count.public_send(math_operator, 2)).to_sql.must_be_like %{
+ COUNT("users"."id") #{math_operator} 2
+ }
+ end
+
+ it "maximum should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].maximum.public_send(math_operator, 2)).to_sql.must_be_like %{
+ MAX("users"."id") #{math_operator} 2
+ }
+ end
+
+ it "minimum should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].minimum.public_send(math_operator, 2)).to_sql.must_be_like %{
+ MIN("users"."id") #{math_operator} 2
+ }
+ end
+
+ it "attribute node should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].public_send(math_operator, 2)).to_sql.must_be_like %{
+ "users"."id" #{math_operator} 2
+ }
+ end
+ end
+
+ %i[+ - & | ^ << >>].each do |math_operator|
+ it "average should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].average.public_send(math_operator, 2)).to_sql.must_be_like %{
+ (AVG("users"."id") #{math_operator} 2)
+ }
+ end
+
+ it "count should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].count.public_send(math_operator, 2)).to_sql.must_be_like %{
+ (COUNT("users"."id") #{math_operator} 2)
+ }
+ end
+
+ it "maximum should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].maximum.public_send(math_operator, 2)).to_sql.must_be_like %{
+ (MAX("users"."id") #{math_operator} 2)
+ }
+ end
+
+ it "minimum should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].minimum.public_send(math_operator, 2)).to_sql.must_be_like %{
+ (MIN("users"."id") #{math_operator} 2)
+ }
+ end
+
+ it "attribute node should be compatible with #{math_operator}" do
+ table = Arel::Table.new :users
+ (table[:id].public_send(math_operator, 2)).to_sql.must_be_like %{
+ ("users"."id" #{math_operator} 2)
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/attributes_test.rb b/activerecord/test/cases/arel/attributes_test.rb
new file mode 100644
index 0000000000..b00af4bd29
--- /dev/null
+++ b/activerecord/test/cases/arel/attributes_test.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ describe "Attributes" do
+ it "responds to lower" do
+ relation = Table.new(:users)
+ attribute = relation[:foo]
+ node = attribute.lower
+ assert_equal "LOWER", node.name
+ assert_equal [attribute], node.expressions
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Attribute.new("foo", "bar"), Attribute.new("foo", "bar")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Attribute.new("foo", "bar"), Attribute.new("foo", "baz")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+
+ describe "for" do
+ it "deals with unknown column types" do
+ column = Struct.new(:type).new :crazy
+ Attributes.for(column).must_equal Attributes::Undefined
+ end
+
+ it "returns the correct constant for strings" do
+ [:string, :text, :binary].each do |type|
+ column = Struct.new(:type).new type
+ Attributes.for(column).must_equal Attributes::String
+ end
+ end
+
+ it "returns the correct constant for ints" do
+ column = Struct.new(:type).new :integer
+ Attributes.for(column).must_equal Attributes::Integer
+ end
+
+ it "returns the correct constant for floats" do
+ column = Struct.new(:type).new :float
+ Attributes.for(column).must_equal Attributes::Float
+ end
+
+ it "returns the correct constant for decimals" do
+ column = Struct.new(:type).new :decimal
+ Attributes.for(column).must_equal Attributes::Decimal
+ end
+
+ it "returns the correct constant for boolean" do
+ column = Struct.new(:type).new :boolean
+ Attributes.for(column).must_equal Attributes::Boolean
+ end
+
+ it "returns the correct constant for time" do
+ [:date, :datetime, :timestamp, :time].each do |type|
+ column = Struct.new(:type).new type
+ Attributes.for(column).must_equal Attributes::Time
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/collectors/bind_test.rb b/activerecord/test/cases/arel/collectors/bind_test.rb
new file mode 100644
index 0000000000..ffa9b15f66
--- /dev/null
+++ b/activerecord/test/cases/arel/collectors/bind_test.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "arel/collectors/bind"
+
+module Arel
+ module Collectors
+ class TestBind < Arel::Test
+ def setup
+ @conn = FakeRecord::Base.new
+ @visitor = Visitors::ToSql.new @conn.connection
+ super
+ end
+
+ def collect(node)
+ @visitor.accept(node, Collectors::Bind.new)
+ end
+
+ def compile(node)
+ collect(node).value
+ end
+
+ def ast_with_binds(bvs)
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.where(table[:age].eq(Nodes::BindParam.new(bvs.shift)))
+ manager.where(table[:name].eq(Nodes::BindParam.new(bvs.shift)))
+ manager.ast
+ end
+
+ def test_compile_gathers_all_bind_params
+ binds = compile(ast_with_binds(["hello", "world"]))
+ assert_equal ["hello", "world"], binds
+
+ binds = compile(ast_with_binds(["hello2", "world3"]))
+ assert_equal ["hello2", "world3"], binds
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/collectors/composite_test.rb b/activerecord/test/cases/arel/collectors/composite_test.rb
new file mode 100644
index 0000000000..545637496f
--- /dev/null
+++ b/activerecord/test/cases/arel/collectors/composite_test.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+require "arel/collectors/bind"
+require "arel/collectors/composite"
+
+module Arel
+ module Collectors
+ class TestComposite < Arel::Test
+ def setup
+ @conn = FakeRecord::Base.new
+ @visitor = Visitors::ToSql.new @conn.connection
+ super
+ end
+
+ def collect(node)
+ sql_collector = Collectors::SQLString.new
+ bind_collector = Collectors::Bind.new
+ collector = Collectors::Composite.new(sql_collector, bind_collector)
+ @visitor.accept(node, collector)
+ end
+
+ def compile(node)
+ collect(node).value
+ end
+
+ def ast_with_binds(bvs)
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.where(table[:age].eq(Nodes::BindParam.new(bvs.shift)))
+ manager.where(table[:name].eq(Nodes::BindParam.new(bvs.shift)))
+ manager.ast
+ end
+
+ def test_composite_collector_performs_multiple_collections_at_once
+ sql, binds = compile(ast_with_binds(["hello", "world"]))
+ assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql
+ assert_equal ["hello", "world"], binds
+
+ sql, binds = compile(ast_with_binds(["hello2", "world3"]))
+ assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql
+ assert_equal ["hello2", "world3"], binds
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/collectors/sql_string_test.rb b/activerecord/test/cases/arel/collectors/sql_string_test.rb
new file mode 100644
index 0000000000..443c7eb54b
--- /dev/null
+++ b/activerecord/test/cases/arel/collectors/sql_string_test.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Collectors
+ class TestSqlString < Arel::Test
+ def setup
+ @conn = FakeRecord::Base.new
+ @visitor = Visitors::ToSql.new @conn.connection
+ super
+ end
+
+ def collect(node)
+ @visitor.accept(node, Collectors::SQLString.new)
+ end
+
+ def compile(node)
+ collect(node).value
+ end
+
+ def ast_with_binds
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.where(table[:age].eq(Nodes::BindParam.new("hello")))
+ manager.where(table[:name].eq(Nodes::BindParam.new("world")))
+ manager.ast
+ end
+
+ def test_compile
+ sql = compile(ast_with_binds)
+ assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql
+ end
+
+ def test_returned_sql_uses_utf8_encoding
+ sql = compile(ast_with_binds)
+ assert_equal sql.encoding, Encoding::UTF_8
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/collectors/substitute_bind_collector_test.rb b/activerecord/test/cases/arel/collectors/substitute_bind_collector_test.rb
new file mode 100644
index 0000000000..255c8e79e9
--- /dev/null
+++ b/activerecord/test/cases/arel/collectors/substitute_bind_collector_test.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "arel/collectors/substitute_binds"
+require "arel/collectors/sql_string"
+
+module Arel
+ module Collectors
+ class TestSubstituteBindCollector < Arel::Test
+ def setup
+ @conn = FakeRecord::Base.new
+ @visitor = Visitors::ToSql.new @conn.connection
+ super
+ end
+
+ def ast_with_binds
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.where(table[:age].eq(Nodes::BindParam.new("hello")))
+ manager.where(table[:name].eq(Nodes::BindParam.new("world")))
+ manager.ast
+ end
+
+ def compile(node, quoter)
+ collector = Collectors::SubstituteBinds.new(quoter, Collectors::SQLString.new)
+ @visitor.accept(node, collector).value
+ end
+
+ def test_compile
+ quoter = Object.new
+ def quoter.quote(val)
+ val.to_s
+ end
+ sql = compile(ast_with_binds, quoter)
+ assert_equal 'SELECT FROM "users" WHERE "users"."age" = hello AND "users"."name" = world', sql
+ end
+
+ def test_quoting_is_delegated_to_quoter
+ quoter = Object.new
+ def quoter.quote(val)
+ val.inspect
+ end
+ sql = compile(ast_with_binds, quoter)
+ assert_equal 'SELECT FROM "users" WHERE "users"."age" = "hello" AND "users"."name" = "world"', sql
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/crud_test.rb b/activerecord/test/cases/arel/crud_test.rb
new file mode 100644
index 0000000000..f3cdd8927f
--- /dev/null
+++ b/activerecord/test/cases/arel/crud_test.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class FakeCrudder < SelectManager
+ class FakeEngine
+ attr_reader :calls, :connection_pool, :spec, :config
+
+ def initialize
+ @calls = []
+ @connection_pool = self
+ @spec = self
+ @config = { adapter: "sqlite3" }
+ end
+
+ def connection; self end
+
+ def method_missing(name, *args)
+ @calls << [name, args]
+ end
+ end
+
+ include Crud
+
+ attr_reader :engine
+ attr_accessor :ctx
+
+ def initialize(engine = FakeEngine.new)
+ super
+ end
+ end
+
+ describe "crud" do
+ describe "insert" do
+ it "should call insert on the connection" do
+ table = Table.new :users
+ fc = FakeCrudder.new
+ fc.from table
+ im = fc.compile_insert [[table[:id], "foo"]]
+ assert_instance_of Arel::InsertManager, im
+ end
+ end
+
+ describe "update" do
+ it "should call update on the connection" do
+ table = Table.new :users
+ fc = FakeCrudder.new
+ fc.from table
+ stmt = fc.compile_update [[table[:id], "foo"]], Arel::Attributes::Attribute.new(table, "id")
+ assert_instance_of Arel::UpdateManager, stmt
+ end
+ end
+
+ describe "delete" do
+ it "should call delete on the connection" do
+ table = Table.new :users
+ fc = FakeCrudder.new
+ fc.from table
+ stmt = fc.compile_delete
+ assert_instance_of Arel::DeleteManager, stmt
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/delete_manager_test.rb b/activerecord/test/cases/arel/delete_manager_test.rb
new file mode 100644
index 0000000000..0bad02f4d2
--- /dev/null
+++ b/activerecord/test/cases/arel/delete_manager_test.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class DeleteManagerTest < Arel::Spec
+ describe "new" do
+ it "takes an engine" do
+ Arel::DeleteManager.new
+ end
+ end
+
+ it "handles limit properly" do
+ table = Table.new(:users)
+ dm = Arel::DeleteManager.new
+ dm.take 10
+ dm.from table
+ dm.key = table[:id]
+ assert_match(/LIMIT 10/, dm.to_sql)
+ end
+
+ describe "from" do
+ it "uses from" do
+ table = Table.new(:users)
+ dm = Arel::DeleteManager.new
+ dm.from table
+ dm.to_sql.must_be_like %{ DELETE FROM "users" }
+ end
+
+ it "chains" do
+ table = Table.new(:users)
+ dm = Arel::DeleteManager.new
+ dm.from(table).must_equal dm
+ end
+ end
+
+ describe "where" do
+ it "uses where values" do
+ table = Table.new(:users)
+ dm = Arel::DeleteManager.new
+ dm.from table
+ dm.where table[:id].eq(10)
+ dm.to_sql.must_be_like %{ DELETE FROM "users" WHERE "users"."id" = 10}
+ end
+
+ it "chains" do
+ table = Table.new(:users)
+ dm = Arel::DeleteManager.new
+ dm.where(table[:id].eq(10)).must_equal dm
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/factory_methods_test.rb b/activerecord/test/cases/arel/factory_methods_test.rb
new file mode 100644
index 0000000000..26d2cdd08d
--- /dev/null
+++ b/activerecord/test/cases/arel/factory_methods_test.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ module FactoryMethods
+ class TestFactoryMethods < Arel::Test
+ class Factory
+ include Arel::FactoryMethods
+ end
+
+ def setup
+ @factory = Factory.new
+ end
+
+ def test_create_join
+ join = @factory.create_join :one, :two
+ assert_kind_of Nodes::Join, join
+ assert_equal :two, join.right
+ end
+
+ def test_create_on
+ on = @factory.create_on :one
+ assert_instance_of Nodes::On, on
+ assert_equal :one, on.expr
+ end
+
+ def test_create_true
+ true_node = @factory.create_true
+ assert_instance_of Nodes::True, true_node
+ end
+
+ def test_create_false
+ false_node = @factory.create_false
+ assert_instance_of Nodes::False, false_node
+ end
+
+ def test_lower
+ lower = @factory.lower :one
+ assert_instance_of Nodes::NamedFunction, lower
+ assert_equal "LOWER", lower.name
+ assert_equal [:one], lower.expressions.map(&:expr)
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/helper.rb b/activerecord/test/cases/arel/helper.rb
new file mode 100644
index 0000000000..f8ce658440
--- /dev/null
+++ b/activerecord/test/cases/arel/helper.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+require "active_support"
+require "minitest/autorun"
+require "arel"
+
+require_relative "support/fake_record"
+
+class Object
+ def must_be_like(other)
+ gsub(/\s+/, " ").strip.must_equal other.gsub(/\s+/, " ").strip
+ end
+end
+
+module Arel
+ class Test < ActiveSupport::TestCase
+ def setup
+ super
+ @arel_engine = Arel::Table.engine
+ Arel::Table.engine = FakeRecord::Base.new
+ end
+
+ def teardown
+ Arel::Table.engine = @arel_engine if defined? @arel_engine
+ super
+ end
+ end
+
+ class Spec < Minitest::Spec
+ before do
+ @arel_engine = Arel::Table.engine
+ Arel::Table.engine = FakeRecord::Base.new
+ end
+
+ after do
+ Arel::Table.engine = @arel_engine if defined? @arel_engine
+ end
+ include ActiveSupport::Testing::Assertions
+
+ # test/unit backwards compatibility methods
+ alias :assert_no_match :refute_match
+ alias :assert_not_equal :refute_equal
+ alias :assert_not_same :refute_same
+ end
+end
diff --git a/activerecord/test/cases/arel/insert_manager_test.rb b/activerecord/test/cases/arel/insert_manager_test.rb
new file mode 100644
index 0000000000..79b85742ee
--- /dev/null
+++ b/activerecord/test/cases/arel/insert_manager_test.rb
@@ -0,0 +1,241 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class InsertManagerTest < Arel::Spec
+ describe "new" do
+ it "takes an engine" do
+ Arel::InsertManager.new
+ end
+ end
+
+ describe "insert" do
+ it "can create a ValuesList node" do
+ manager = Arel::InsertManager.new
+ values = manager.create_values_list([%w{ a b }, %w{ c d }])
+
+ assert_kind_of Arel::Nodes::ValuesList, values
+ assert_equal [%w{ a b }, %w{ c d }], values.rows
+ end
+
+ it "allows sql literals" do
+ manager = Arel::InsertManager.new
+ manager.into Table.new(:users)
+ manager.values = manager.create_values([Arel.sql("*")])
+ manager.to_sql.must_be_like %{
+ INSERT INTO \"users\" VALUES (*)
+ }
+ end
+
+ it "works with multiple values" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.columns << table[:id]
+ manager.columns << table[:name]
+
+ manager.values = manager.create_values_list([
+ %w{1 david},
+ %w{2 kir},
+ ["3", Arel.sql("DEFAULT")],
+ ])
+
+ manager.to_sql.must_be_like %{
+ INSERT INTO \"users\" (\"id\", \"name\") VALUES ('1', 'david'), ('2', 'kir'), ('3', DEFAULT)
+ }
+ end
+
+ it "literals in multiple values are not escaped" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.columns << table[:name]
+
+ manager.values = manager.create_values_list([
+ [Arel.sql("*")],
+ [Arel.sql("DEFAULT")],
+ ])
+
+ manager.to_sql.must_be_like %{
+ INSERT INTO \"users\" (\"name\") VALUES (*), (DEFAULT)
+ }
+ end
+
+ it "works with multiple single values" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.columns << table[:name]
+
+ manager.values = manager.create_values_list([
+ %w{david},
+ %w{kir},
+ [Arel.sql("DEFAULT")],
+ ])
+
+ manager.to_sql.must_be_like %{
+ INSERT INTO \"users\" (\"name\") VALUES ('david'), ('kir'), (DEFAULT)
+ }
+ end
+
+ it "inserts false" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+
+ manager.insert [[table[:bool], false]]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("bool") VALUES ('f')
+ }
+ end
+
+ it "inserts null" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.insert [[table[:id], nil]]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id") VALUES (NULL)
+ }
+ end
+
+ it "inserts time" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+
+ time = Time.now
+ attribute = table[:created_at]
+
+ manager.insert [[attribute, time]]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("created_at") VALUES (#{Table.engine.connection.quote time})
+ }
+ end
+
+ it "takes a list of lists" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.into table
+ manager.insert [[table[:id], 1], [table[:name], "aaron"]]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id", "name") VALUES (1, 'aaron')
+ }
+ end
+
+ it "defaults the table" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.insert [[table[:id], 1], [table[:name], "aaron"]]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id", "name") VALUES (1, 'aaron')
+ }
+ end
+
+ it "noop for empty list" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ manager.insert [[table[:id], 1]]
+ manager.insert []
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id") VALUES (1)
+ }
+ end
+
+ it "is chainable" do
+ table = Table.new(:users)
+ manager = Arel::InsertManager.new
+ insert_result = manager.insert [[table[:id], 1]]
+ assert_equal manager, insert_result
+ end
+ end
+
+ describe "into" do
+ it "takes a Table and chains" do
+ manager = Arel::InsertManager.new
+ manager.into(Table.new(:users)).must_equal manager
+ end
+
+ it "converts to sql" do
+ table = Table.new :users
+ manager = Arel::InsertManager.new
+ manager.into table
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users"
+ }
+ end
+ end
+
+ describe "columns" do
+ it "converts to sql" do
+ table = Table.new :users
+ manager = Arel::InsertManager.new
+ manager.into table
+ manager.columns << table[:id]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id")
+ }
+ end
+ end
+
+ describe "values" do
+ it "converts to sql" do
+ table = Table.new :users
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.values = Nodes::ValuesList.new([[1], [2]])
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" VALUES (1), (2)
+ }
+ end
+
+ it "accepts sql literals" do
+ table = Table.new :users
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.values = Arel.sql("DEFAULT VALUES")
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" DEFAULT VALUES
+ }
+ end
+ end
+
+ describe "combo" do
+ it "combines columns and values list in order" do
+ table = Table.new :users
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ manager.values = Nodes::ValuesList.new([[1, "aaron"], [2, "david"]])
+ manager.columns << table[:id]
+ manager.columns << table[:name]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id", "name") VALUES (1, 'aaron'), (2, 'david')
+ }
+ end
+ end
+
+ describe "select" do
+ it "accepts a select query in place of a VALUES clause" do
+ table = Table.new :users
+
+ manager = Arel::InsertManager.new
+ manager.into table
+
+ select = Arel::SelectManager.new
+ select.project Arel.sql("1")
+ select.project Arel.sql('"aaron"')
+
+ manager.select select
+ manager.columns << table[:id]
+ manager.columns << table[:name]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id", "name") (SELECT 1, "aaron")
+ }
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/and_test.rb b/activerecord/test/cases/arel/nodes/and_test.rb
new file mode 100644
index 0000000000..d123ca9fd0
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/and_test.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "And" do
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [And.new(["foo", "bar"]), And.new(["foo", "bar"])]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [And.new(["foo", "bar"]), And.new(["foo", "baz"])]
+ assert_equal 2, array.uniq.size
+ end
+ end
+
+ describe "functions as node expression" do
+ it "allows aliasing" do
+ aliased = And.new(["foo", "bar"]).as("baz")
+
+ assert_kind_of As, aliased
+ assert_kind_of SqlLiteral, aliased.right
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/as_test.rb b/activerecord/test/cases/arel/nodes/as_test.rb
new file mode 100644
index 0000000000..1169ea11c9
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/as_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "As" do
+ describe "#as" do
+ it "makes an AS node" do
+ attr = Table.new(:users)[:id]
+ as = attr.as(Arel.sql("foo"))
+ assert_equal attr, as.left
+ assert_equal "foo", as.right
+ end
+
+ it "converts right to SqlLiteral if a string" do
+ attr = Table.new(:users)[:id]
+ as = attr.as("foo")
+ assert_kind_of Arel::Nodes::SqlLiteral, as.right
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [As.new("foo", "bar"), As.new("foo", "bar")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [As.new("foo", "bar"), As.new("foo", "baz")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/ascending_test.rb b/activerecord/test/cases/arel/nodes/ascending_test.rb
new file mode 100644
index 0000000000..4811e6ff5b
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/ascending_test.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestAscending < Arel::Test
+ def test_construct
+ ascending = Ascending.new "zomg"
+ assert_equal "zomg", ascending.expr
+ end
+
+ def test_reverse
+ ascending = Ascending.new "zomg"
+ descending = ascending.reverse
+ assert_kind_of Descending, descending
+ assert_equal ascending.expr, descending.expr
+ end
+
+ def test_direction
+ ascending = Ascending.new "zomg"
+ assert_equal :asc, ascending.direction
+ end
+
+ def test_ascending?
+ ascending = Ascending.new "zomg"
+ assert ascending.ascending?
+ end
+
+ def test_descending?
+ ascending = Ascending.new "zomg"
+ assert_not ascending.descending?
+ end
+
+ def test_equality_with_same_ivars
+ array = [Ascending.new("zomg"), Ascending.new("zomg")]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [Ascending.new("zomg"), Ascending.new("zomg!")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/bin_test.rb b/activerecord/test/cases/arel/nodes/bin_test.rb
new file mode 100644
index 0000000000..ee2ec3cf2f
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/bin_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestBin < Arel::Test
+ def test_new
+ assert Arel::Nodes::Bin.new("zomg")
+ end
+
+ def test_default_to_sql
+ viz = Arel::Visitors::ToSql.new Table.engine.connection_pool
+ node = Arel::Nodes::Bin.new(Arel.sql("zomg"))
+ assert_equal "zomg", viz.accept(node, Collectors::SQLString.new).value
+ end
+
+ def test_mysql_to_sql
+ viz = Arel::Visitors::MySQL.new Table.engine.connection_pool
+ node = Arel::Nodes::Bin.new(Arel.sql("zomg"))
+ assert_equal "BINARY zomg", viz.accept(node, Collectors::SQLString.new).value
+ end
+
+ def test_equality_with_same_ivars
+ array = [Bin.new("zomg"), Bin.new("zomg")]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [Bin.new("zomg"), Bin.new("zomg!")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/binary_test.rb b/activerecord/test/cases/arel/nodes/binary_test.rb
new file mode 100644
index 0000000000..d160e7cd9d
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/binary_test.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class NodesTest < Arel::Spec
+ describe "Binary" do
+ describe "#hash" do
+ it "generates a hash based on its value" do
+ eq = Equality.new("foo", "bar")
+ eq2 = Equality.new("foo", "bar")
+ eq3 = Equality.new("bar", "baz")
+
+ assert_equal eq.hash, eq2.hash
+ assert_not_equal eq.hash, eq3.hash
+ end
+
+ it "generates a hash specific to its class" do
+ eq = Equality.new("foo", "bar")
+ neq = NotEqual.new("foo", "bar")
+
+ assert_not_equal eq.hash, neq.hash
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/bind_param_test.rb b/activerecord/test/cases/arel/nodes/bind_param_test.rb
new file mode 100644
index 0000000000..37a362ece4
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/bind_param_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "BindParam" do
+ it "is equal to other bind params with the same value" do
+ BindParam.new(1).must_equal(BindParam.new(1))
+ BindParam.new("foo").must_equal(BindParam.new("foo"))
+ end
+
+ it "is not equal to other nodes" do
+ BindParam.new(nil).wont_equal(Node.new)
+ end
+
+ it "is not equal to bind params with different values" do
+ BindParam.new(1).wont_equal(BindParam.new(2))
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/case_test.rb b/activerecord/test/cases/arel/nodes/case_test.rb
new file mode 100644
index 0000000000..946c2b0453
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/case_test.rb
@@ -0,0 +1,96 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class NodesTest < Arel::Spec
+ describe "Case" do
+ describe "#initialize" do
+ it "sets case expression from first argument" do
+ node = Case.new "foo"
+
+ assert_equal "foo", node.case
+ end
+
+ it "sets default case from second argument" do
+ node = Case.new nil, "bar"
+
+ assert_equal "bar", node.default
+ end
+ end
+
+ describe "#clone" do
+ it "clones case, conditions and default" do
+ foo = Nodes.build_quoted "foo"
+
+ node = Case.new
+ node.case = foo
+ node.conditions = [When.new(foo, foo)]
+ node.default = foo
+
+ dolly = node.clone
+
+ assert_equal dolly.case, node.case
+ assert_not_same dolly.case, node.case
+
+ assert_equal dolly.conditions, node.conditions
+ assert_not_same dolly.conditions, node.conditions
+
+ assert_equal dolly.default, node.default
+ assert_not_same dolly.default, node.default
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ foo = Nodes.build_quoted "foo"
+ one = Nodes.build_quoted 1
+ zero = Nodes.build_quoted 0
+
+ case1 = Case.new foo
+ case1.conditions = [When.new(foo, one)]
+ case1.default = Else.new zero
+
+ case2 = Case.new foo
+ case2.conditions = [When.new(foo, one)]
+ case2.default = Else.new zero
+
+ array = [case1, case2]
+
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ foo = Nodes.build_quoted "foo"
+ bar = Nodes.build_quoted "bar"
+ one = Nodes.build_quoted 1
+ zero = Nodes.build_quoted 0
+
+ case1 = Case.new foo
+ case1.conditions = [When.new(foo, one)]
+ case1.default = Else.new zero
+
+ case2 = Case.new foo
+ case2.conditions = [When.new(bar, one)]
+ case2.default = Else.new zero
+
+ array = [case1, case2]
+
+ assert_equal 2, array.uniq.size
+ end
+ end
+
+ describe "#as" do
+ it "allows aliasing" do
+ node = Case.new "foo"
+ as = node.as("bar")
+
+ assert_equal node, as.left
+ assert_kind_of Arel::Nodes::SqlLiteral, as.right
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/casted_test.rb b/activerecord/test/cases/arel/nodes/casted_test.rb
new file mode 100644
index 0000000000..e27f58a4e2
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/casted_test.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe Casted do
+ describe "#hash" do
+ it "is equal when eql? returns true" do
+ one = Casted.new 1, 2
+ also_one = Casted.new 1, 2
+
+ assert_equal one.hash, also_one.hash
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/comment_test.rb b/activerecord/test/cases/arel/nodes/comment_test.rb
new file mode 100644
index 0000000000..bf5eaf4c5a
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/comment_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "yaml"
+
+module Arel
+ module Nodes
+ class CommentTest < Arel::Spec
+ describe "equality" do
+ it "is equal with equal contents" do
+ array = [Comment.new(["foo"]), Comment.new(["foo"])]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different contents" do
+ array = [Comment.new(["foo"]), Comment.new(["bar"])]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/count_test.rb b/activerecord/test/cases/arel/nodes/count_test.rb
new file mode 100644
index 0000000000..daabea6c4c
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/count_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+class Arel::Nodes::CountTest < Arel::Spec
+ describe "as" do
+ it "should alias the count" do
+ table = Arel::Table.new :users
+ table[:id].count.as("foo").to_sql.must_be_like %{
+ COUNT("users"."id") AS foo
+ }
+ end
+ end
+
+ describe "eq" do
+ it "should compare the count" do
+ table = Arel::Table.new :users
+ table[:id].count.eq(2).to_sql.must_be_like %{
+ COUNT("users"."id") = 2
+ }
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Arel::Nodes::Count.new("foo"), Arel::Nodes::Count.new("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Arel::Nodes::Count.new("foo"), Arel::Nodes::Count.new("foo!")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/delete_statement_test.rb b/activerecord/test/cases/arel/nodes/delete_statement_test.rb
new file mode 100644
index 0000000000..3f078063a4
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/delete_statement_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+describe Arel::Nodes::DeleteStatement do
+ describe "#clone" do
+ it "clones wheres" do
+ statement = Arel::Nodes::DeleteStatement.new
+ statement.wheres = %w[a b c]
+
+ dolly = statement.clone
+ dolly.wheres.must_equal statement.wheres
+ dolly.wheres.wont_be_same_as statement.wheres
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ statement1 = Arel::Nodes::DeleteStatement.new
+ statement1.wheres = %w[a b c]
+ statement2 = Arel::Nodes::DeleteStatement.new
+ statement2.wheres = %w[a b c]
+ array = [statement1, statement2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ statement1 = Arel::Nodes::DeleteStatement.new
+ statement1.wheres = %w[a b c]
+ statement2 = Arel::Nodes::DeleteStatement.new
+ statement2.wheres = %w[1 2 3]
+ array = [statement1, statement2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/descending_test.rb b/activerecord/test/cases/arel/nodes/descending_test.rb
new file mode 100644
index 0000000000..5f1747e1da
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/descending_test.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestDescending < Arel::Test
+ def test_construct
+ descending = Descending.new "zomg"
+ assert_equal "zomg", descending.expr
+ end
+
+ def test_reverse
+ descending = Descending.new "zomg"
+ ascending = descending.reverse
+ assert_kind_of Ascending, ascending
+ assert_equal descending.expr, ascending.expr
+ end
+
+ def test_direction
+ descending = Descending.new "zomg"
+ assert_equal :desc, descending.direction
+ end
+
+ def test_ascending?
+ descending = Descending.new "zomg"
+ assert_not descending.ascending?
+ end
+
+ def test_descending?
+ descending = Descending.new "zomg"
+ assert descending.descending?
+ end
+
+ def test_equality_with_same_ivars
+ array = [Descending.new("zomg"), Descending.new("zomg")]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [Descending.new("zomg"), Descending.new("zomg!")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/distinct_test.rb b/activerecord/test/cases/arel/nodes/distinct_test.rb
new file mode 100644
index 0000000000..de5f0ee588
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/distinct_test.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "Distinct" do
+ describe "equality" do
+ it "is equal to other distinct nodes" do
+ array = [Distinct.new, Distinct.new]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with other nodes" do
+ array = [Distinct.new, Node.new]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/equality_test.rb b/activerecord/test/cases/arel/nodes/equality_test.rb
new file mode 100644
index 0000000000..e173720e86
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/equality_test.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "equality" do
+ # FIXME: backwards compat
+ describe "backwards compat" do
+ describe "operator" do
+ it "returns :==" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ left.operator.must_equal :==
+ end
+ end
+
+ describe "operand1" do
+ it "should equal left" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ left.left.must_equal left.operand1
+ end
+ end
+
+ describe "operand2" do
+ it "should equal right" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ left.right.must_equal left.operand2
+ end
+ end
+
+ describe "to_sql" do
+ it "takes an engine" do
+ engine = FakeRecord::Base.new
+ engine.connection.extend Module.new {
+ attr_accessor :quote_count
+ def quote(*args) @quote_count += 1; super; end
+ def quote_column_name(*args) @quote_count += 1; super; end
+ def quote_table_name(*args) @quote_count += 1; super; end
+ }
+ engine.connection.quote_count = 0
+
+ attr = Table.new(:users)[:id]
+ test = attr.eq(10)
+ test.to_sql engine
+ engine.connection.quote_count.must_equal 3
+ end
+ end
+ end
+
+ describe "or" do
+ it "makes an OR node" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ right = attr.eq(11)
+ node = left.or right
+ node.expr.left.must_equal left
+ node.expr.right.must_equal right
+ end
+ end
+
+ describe "and" do
+ it "makes and AND node" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ right = attr.eq(11)
+ node = left.and right
+ node.left.must_equal left
+ node.right.must_equal right
+ end
+ end
+
+ it "is equal with equal ivars" do
+ array = [Equality.new("foo", "bar"), Equality.new("foo", "bar")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Equality.new("foo", "bar"), Equality.new("foo", "baz")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/extract_test.rb b/activerecord/test/cases/arel/nodes/extract_test.rb
new file mode 100644
index 0000000000..8fc1e04d67
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/extract_test.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+class Arel::Nodes::ExtractTest < Arel::Spec
+ it "should extract field" do
+ table = Arel::Table.new :users
+ table[:timestamp].extract("date").to_sql.must_be_like %{
+ EXTRACT(DATE FROM "users"."timestamp")
+ }
+ end
+
+ describe "as" do
+ it "should alias the extract" do
+ table = Arel::Table.new :users
+ table[:timestamp].extract("date").as("foo").to_sql.must_be_like %{
+ EXTRACT(DATE FROM "users"."timestamp") AS foo
+ }
+ end
+
+ it "should not mutate the extract" do
+ table = Arel::Table.new :users
+ extract = table[:timestamp].extract("date")
+ before = extract.dup
+ extract.as("foo")
+ assert_equal extract, before
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ table = Arel::Table.new :users
+ array = [table[:attr].extract("foo"), table[:attr].extract("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ table = Arel::Table.new :users
+ array = [table[:attr].extract("foo"), table[:attr].extract("bar")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/false_test.rb b/activerecord/test/cases/arel/nodes/false_test.rb
new file mode 100644
index 0000000000..4ecf8e332e
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/false_test.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "False" do
+ describe "equality" do
+ it "is equal to other false nodes" do
+ array = [False.new, False.new]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with other nodes" do
+ array = [False.new, Node.new]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/grouping_test.rb b/activerecord/test/cases/arel/nodes/grouping_test.rb
new file mode 100644
index 0000000000..03d5c142d5
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/grouping_test.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class GroupingTest < Arel::Spec
+ it "should create Equality nodes" do
+ grouping = Grouping.new(Nodes.build_quoted("foo"))
+ grouping.eq("foo").to_sql.must_be_like "('foo') = 'foo'"
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Grouping.new("foo"), Grouping.new("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Grouping.new("foo"), Grouping.new("bar")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/infix_operation_test.rb b/activerecord/test/cases/arel/nodes/infix_operation_test.rb
new file mode 100644
index 0000000000..dcf2200c12
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/infix_operation_test.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestInfixOperation < Arel::Test
+ def test_construct
+ operation = InfixOperation.new :+, 1, 2
+ assert_equal :+, operation.operator
+ assert_equal 1, operation.left
+ assert_equal 2, operation.right
+ end
+
+ def test_operation_alias
+ operation = InfixOperation.new :+, 1, 2
+ aliaz = operation.as("zomg")
+ assert_kind_of As, aliaz
+ assert_equal operation, aliaz.left
+ assert_equal "zomg", aliaz.right
+ end
+
+ def test_operation_ordering
+ operation = InfixOperation.new :+, 1, 2
+ ordering = operation.desc
+ assert_kind_of Descending, ordering
+ assert_equal operation, ordering.expr
+ assert ordering.descending?
+ end
+
+ def test_equality_with_same_ivars
+ array = [InfixOperation.new(:+, 1, 2), InfixOperation.new(:+, 1, 2)]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [InfixOperation.new(:+, 1, 2), InfixOperation.new(:+, 1, 3)]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/insert_statement_test.rb b/activerecord/test/cases/arel/nodes/insert_statement_test.rb
new file mode 100644
index 0000000000..252a0d0d0b
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/insert_statement_test.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+describe Arel::Nodes::InsertStatement do
+ describe "#clone" do
+ it "clones columns and values" do
+ statement = Arel::Nodes::InsertStatement.new
+ statement.columns = %w[a b c]
+ statement.values = %w[x y z]
+
+ dolly = statement.clone
+ dolly.columns.must_equal statement.columns
+ dolly.values.must_equal statement.values
+
+ dolly.columns.wont_be_same_as statement.columns
+ dolly.values.wont_be_same_as statement.values
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ statement1 = Arel::Nodes::InsertStatement.new
+ statement1.columns = %w[a b c]
+ statement1.values = %w[x y z]
+ statement2 = Arel::Nodes::InsertStatement.new
+ statement2.columns = %w[a b c]
+ statement2.values = %w[x y z]
+ array = [statement1, statement2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ statement1 = Arel::Nodes::InsertStatement.new
+ statement1.columns = %w[a b c]
+ statement1.values = %w[x y z]
+ statement2 = Arel::Nodes::InsertStatement.new
+ statement2.columns = %w[a b c]
+ statement2.values = %w[1 2 3]
+ array = [statement1, statement2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/named_function_test.rb b/activerecord/test/cases/arel/nodes/named_function_test.rb
new file mode 100644
index 0000000000..dbd7ae43be
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/named_function_test.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestNamedFunction < Arel::Test
+ def test_construct
+ function = NamedFunction.new "omg", "zomg"
+ assert_equal "omg", function.name
+ assert_equal "zomg", function.expressions
+ end
+
+ def test_function_alias
+ function = NamedFunction.new "omg", "zomg"
+ function = function.as("wth")
+ assert_equal "omg", function.name
+ assert_equal "zomg", function.expressions
+ assert_kind_of SqlLiteral, function.alias
+ assert_equal "wth", function.alias
+ end
+
+ def test_construct_with_alias
+ function = NamedFunction.new "omg", "zomg", "wth"
+ assert_equal "omg", function.name
+ assert_equal "zomg", function.expressions
+ assert_kind_of SqlLiteral, function.alias
+ assert_equal "wth", function.alias
+ end
+
+ def test_equality_with_same_ivars
+ array = [
+ NamedFunction.new("omg", "zomg", "wth"),
+ NamedFunction.new("omg", "zomg", "wth")
+ ]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [
+ NamedFunction.new("omg", "zomg", "wth"),
+ NamedFunction.new("zomg", "zomg", "wth")
+ ]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/node_test.rb b/activerecord/test/cases/arel/nodes/node_test.rb
new file mode 100644
index 0000000000..f4f07ef2c5
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/node_test.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ class TestNode < Arel::Test
+ def test_includes_factory_methods
+ assert Node.new.respond_to?(:create_join)
+ end
+
+ def test_all_nodes_are_nodes
+ Nodes.constants.map { |k|
+ Nodes.const_get(k)
+ }.grep(Class).each do |klass|
+ next if Nodes::SqlLiteral == klass
+ next if Nodes::BindParam == klass
+ next if klass.name =~ /^Arel::Nodes::(?:Test|.*Test$)/
+ assert klass.ancestors.include?(Nodes::Node), klass.name
+ end
+ end
+
+ def test_each
+ list = []
+ node = Nodes::Node.new
+ node.each { |n| list << n }
+ assert_equal [node], list
+ end
+
+ def test_generator
+ list = []
+ node = Nodes::Node.new
+ node.each.each { |n| list << n }
+ assert_equal [node], list
+ end
+
+ def test_enumerable
+ node = Nodes::Node.new
+ assert_kind_of Enumerable, node
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/not_test.rb b/activerecord/test/cases/arel/nodes/not_test.rb
new file mode 100644
index 0000000000..481e678700
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/not_test.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "not" do
+ describe "#not" do
+ it "makes a NOT node" do
+ attr = Table.new(:users)[:id]
+ expr = attr.eq(10)
+ node = expr.not
+ node.must_be_kind_of Not
+ node.expr.must_equal expr
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Not.new("foo"), Not.new("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Not.new("foo"), Not.new("baz")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/or_test.rb b/activerecord/test/cases/arel/nodes/or_test.rb
new file mode 100644
index 0000000000..93f826740d
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/or_test.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "or" do
+ describe "#or" do
+ it "makes an OR node" do
+ attr = Table.new(:users)[:id]
+ left = attr.eq(10)
+ right = attr.eq(11)
+ node = left.or right
+ node.expr.left.must_equal left
+ node.expr.right.must_equal right
+
+ oror = node.or(right)
+ oror.expr.left.must_equal node
+ oror.expr.right.must_equal right
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Or.new("foo", "bar"), Or.new("foo", "bar")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Or.new("foo", "bar"), Or.new("foo", "baz")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/over_test.rb b/activerecord/test/cases/arel/nodes/over_test.rb
new file mode 100644
index 0000000000..981ec2e34b
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/over_test.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+class Arel::Nodes::OverTest < Arel::Spec
+ describe "as" do
+ it "should alias the expression" do
+ table = Arel::Table.new :users
+ table[:id].count.over.as("foo").to_sql.must_be_like %{
+ COUNT("users"."id") OVER () AS foo
+ }
+ end
+ end
+
+ describe "with literal" do
+ it "should reference the window definition by name" do
+ table = Arel::Table.new :users
+ table[:id].count.over("foo").to_sql.must_be_like %{
+ COUNT("users"."id") OVER "foo"
+ }
+ end
+ end
+
+ describe "with SQL literal" do
+ it "should reference the window definition by name" do
+ table = Arel::Table.new :users
+ table[:id].count.over(Arel.sql("foo")).to_sql.must_be_like %{
+ COUNT("users"."id") OVER foo
+ }
+ end
+ end
+
+ describe "with no expression" do
+ it "should use empty definition" do
+ table = Arel::Table.new :users
+ table[:id].count.over.to_sql.must_be_like %{
+ COUNT("users"."id") OVER ()
+ }
+ end
+ end
+
+ describe "with expression" do
+ it "should use definition in sub-expression" do
+ table = Arel::Table.new :users
+ window = Arel::Nodes::Window.new.order(table["foo"])
+ table[:id].count.over(window).to_sql.must_be_like %{
+ COUNT("users"."id") OVER (ORDER BY \"users\".\"foo\")
+ }
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [
+ Arel::Nodes::Over.new("foo", "bar"),
+ Arel::Nodes::Over.new("foo", "bar")
+ ]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [
+ Arel::Nodes::Over.new("foo", "bar"),
+ Arel::Nodes::Over.new("foo", "baz")
+ ]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/select_core_test.rb b/activerecord/test/cases/arel/nodes/select_core_test.rb
new file mode 100644
index 0000000000..6860f2a395
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/select_core_test.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestSelectCore < Arel::Test
+ def test_clone
+ core = Arel::Nodes::SelectCore.new
+ core.froms = %w[a b c]
+ core.projections = %w[d e f]
+ core.wheres = %w[g h i]
+
+ dolly = core.clone
+
+ assert_equal core.froms, dolly.froms
+ assert_equal core.projections, dolly.projections
+ assert_equal core.wheres, dolly.wheres
+
+ assert_not_same core.froms, dolly.froms
+ assert_not_same core.projections, dolly.projections
+ assert_not_same core.wheres, dolly.wheres
+ end
+
+ def test_set_quantifier
+ core = Arel::Nodes::SelectCore.new
+ core.set_quantifier = Arel::Nodes::Distinct.new
+ viz = Arel::Visitors::ToSql.new Table.engine.connection_pool
+ assert_match "DISTINCT", viz.accept(core, Collectors::SQLString.new).value
+ end
+
+ def test_equality_with_same_ivars
+ core1 = SelectCore.new
+ core1.froms = %w[a b c]
+ core1.projections = %w[d e f]
+ core1.wheres = %w[g h i]
+ core1.groups = %w[j k l]
+ core1.windows = %w[m n o]
+ core1.havings = %w[p q r]
+ core1.comment = Arel::Nodes::Comment.new(["comment"])
+ core2 = SelectCore.new
+ core2.froms = %w[a b c]
+ core2.projections = %w[d e f]
+ core2.wheres = %w[g h i]
+ core2.groups = %w[j k l]
+ core2.windows = %w[m n o]
+ core2.havings = %w[p q r]
+ core2.comment = Arel::Nodes::Comment.new(["comment"])
+ array = [core1, core2]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ core1 = SelectCore.new
+ core1.froms = %w[a b c]
+ core1.projections = %w[d e f]
+ core1.wheres = %w[g h i]
+ core1.groups = %w[j k l]
+ core1.windows = %w[m n o]
+ core1.havings = %w[p q r]
+ core1.comment = Arel::Nodes::Comment.new(["comment"])
+ core2 = SelectCore.new
+ core2.froms = %w[a b c]
+ core2.projections = %w[d e f]
+ core2.wheres = %w[g h i]
+ core2.groups = %w[j k l]
+ core2.windows = %w[m n o]
+ core2.havings = %w[l o l]
+ core2.comment = Arel::Nodes::Comment.new(["comment"])
+ array = [core1, core2]
+ assert_equal 2, array.uniq.size
+ core2.havings = %w[p q r]
+ core2.comment = Arel::Nodes::Comment.new(["other"])
+ array = [core1, core2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/select_statement_test.rb b/activerecord/test/cases/arel/nodes/select_statement_test.rb
new file mode 100644
index 0000000000..a91605de3e
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/select_statement_test.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+describe Arel::Nodes::SelectStatement do
+ describe "#clone" do
+ it "clones cores" do
+ statement = Arel::Nodes::SelectStatement.new %w[a b c]
+
+ dolly = statement.clone
+ dolly.cores.must_equal statement.cores
+ dolly.cores.wont_be_same_as statement.cores
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ statement1 = Arel::Nodes::SelectStatement.new %w[a b c]
+ statement1.offset = 1
+ statement1.limit = 2
+ statement1.lock = false
+ statement1.orders = %w[x y z]
+ statement1.with = "zomg"
+ statement2 = Arel::Nodes::SelectStatement.new %w[a b c]
+ statement2.offset = 1
+ statement2.limit = 2
+ statement2.lock = false
+ statement2.orders = %w[x y z]
+ statement2.with = "zomg"
+ array = [statement1, statement2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ statement1 = Arel::Nodes::SelectStatement.new %w[a b c]
+ statement1.offset = 1
+ statement1.limit = 2
+ statement1.lock = false
+ statement1.orders = %w[x y z]
+ statement1.with = "zomg"
+ statement2 = Arel::Nodes::SelectStatement.new %w[a b c]
+ statement2.offset = 1
+ statement2.limit = 2
+ statement2.lock = false
+ statement2.orders = %w[x y z]
+ statement2.with = "wth"
+ array = [statement1, statement2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/sql_literal_test.rb b/activerecord/test/cases/arel/nodes/sql_literal_test.rb
new file mode 100644
index 0000000000..3b95fed1f4
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/sql_literal_test.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "yaml"
+
+module Arel
+ module Nodes
+ class SqlLiteralTest < Arel::Spec
+ before do
+ @visitor = Visitors::ToSql.new Table.engine.connection
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ describe "sql" do
+ it "makes a sql literal node" do
+ sql = Arel.sql "foo"
+ sql.must_be_kind_of Arel::Nodes::SqlLiteral
+ end
+ end
+
+ describe "count" do
+ it "makes a count node" do
+ node = SqlLiteral.new("*").count
+ compile(node).must_be_like %{ COUNT(*) }
+ end
+
+ it "makes a distinct node" do
+ node = SqlLiteral.new("*").count true
+ compile(node).must_be_like %{ COUNT(DISTINCT *) }
+ end
+ end
+
+ describe "equality" do
+ it "makes an equality node" do
+ node = SqlLiteral.new("foo").eq(1)
+ compile(node).must_be_like %{ foo = 1 }
+ end
+
+ it "is equal with equal contents" do
+ array = [SqlLiteral.new("foo"), SqlLiteral.new("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different contents" do
+ array = [SqlLiteral.new("foo"), SqlLiteral.new("bar")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+
+ describe 'grouped "or" equality' do
+ it "makes a grouping node with an or node" do
+ node = SqlLiteral.new("foo").eq_any([1, 2])
+ compile(node).must_be_like %{ (foo = 1 OR foo = 2) }
+ end
+ end
+
+ describe 'grouped "and" equality' do
+ it "makes a grouping node with an and node" do
+ node = SqlLiteral.new("foo").eq_all([1, 2])
+ compile(node).must_be_like %{ (foo = 1 AND foo = 2) }
+ end
+ end
+
+ describe "serialization" do
+ it "serializes into YAML" do
+ yaml_literal = SqlLiteral.new("foo").to_yaml
+ assert_equal("foo", YAML.load(yaml_literal))
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/sum_test.rb b/activerecord/test/cases/arel/nodes/sum_test.rb
new file mode 100644
index 0000000000..5015964951
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/sum_test.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+class Arel::Nodes::SumTest < Arel::Spec
+ describe "as" do
+ it "should alias the sum" do
+ table = Arel::Table.new :users
+ table[:id].sum.as("foo").to_sql.must_be_like %{
+ SUM("users"."id") AS foo
+ }
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ array = [Arel::Nodes::Sum.new("foo"), Arel::Nodes::Sum.new("foo")]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ array = [Arel::Nodes::Sum.new("foo"), Arel::Nodes::Sum.new("foo!")]
+ assert_equal 2, array.uniq.size
+ end
+ end
+
+ describe "order" do
+ it "should order the sum" do
+ table = Arel::Table.new :users
+ table[:id].sum.desc.to_sql.must_be_like %{
+ SUM("users"."id") DESC
+ }
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/table_alias_test.rb b/activerecord/test/cases/arel/nodes/table_alias_test.rb
new file mode 100644
index 0000000000..c661b6771e
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/table_alias_test.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "table alias" do
+ describe "equality" do
+ it "is equal with equal ivars" do
+ relation1 = Table.new(:users)
+ node1 = TableAlias.new relation1, :foo
+ relation2 = Table.new(:users)
+ node2 = TableAlias.new relation2, :foo
+ array = [node1, node2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ relation1 = Table.new(:users)
+ node1 = TableAlias.new relation1, :foo
+ relation2 = Table.new(:users)
+ node2 = TableAlias.new relation2, :bar
+ array = [node1, node2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/true_test.rb b/activerecord/test/cases/arel/nodes/true_test.rb
new file mode 100644
index 0000000000..1e85fe7d48
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/true_test.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "True" do
+ describe "equality" do
+ it "is equal to other true nodes" do
+ array = [True.new, True.new]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with other nodes" do
+ array = [True.new, Node.new]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/unary_operation_test.rb b/activerecord/test/cases/arel/nodes/unary_operation_test.rb
new file mode 100644
index 0000000000..f0dd0c625c
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/unary_operation_test.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ class TestUnaryOperation < Arel::Test
+ def test_construct
+ operation = UnaryOperation.new :-, 1
+ assert_equal :-, operation.operator
+ assert_equal 1, operation.expr
+ end
+
+ def test_operation_alias
+ operation = UnaryOperation.new :-, 1
+ aliaz = operation.as("zomg")
+ assert_kind_of As, aliaz
+ assert_equal operation, aliaz.left
+ assert_equal "zomg", aliaz.right
+ end
+
+ def test_operation_ordering
+ operation = UnaryOperation.new :-, 1
+ ordering = operation.desc
+ assert_kind_of Descending, ordering
+ assert_equal operation, ordering.expr
+ assert ordering.descending?
+ end
+
+ def test_equality_with_same_ivars
+ array = [UnaryOperation.new(:-, 1), UnaryOperation.new(:-, 1)]
+ assert_equal 1, array.uniq.size
+ end
+
+ def test_inequality_with_different_ivars
+ array = [UnaryOperation.new(:-, 1), UnaryOperation.new(:-, 2)]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/update_statement_test.rb b/activerecord/test/cases/arel/nodes/update_statement_test.rb
new file mode 100644
index 0000000000..a83ce32f68
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/update_statement_test.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+describe Arel::Nodes::UpdateStatement do
+ describe "#clone" do
+ it "clones wheres and values" do
+ statement = Arel::Nodes::UpdateStatement.new
+ statement.wheres = %w[a b c]
+ statement.values = %w[x y z]
+
+ dolly = statement.clone
+ dolly.wheres.must_equal statement.wheres
+ dolly.wheres.wont_be_same_as statement.wheres
+
+ dolly.values.must_equal statement.values
+ dolly.values.wont_be_same_as statement.values
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ statement1 = Arel::Nodes::UpdateStatement.new
+ statement1.relation = "zomg"
+ statement1.wheres = 2
+ statement1.values = false
+ statement1.orders = %w[x y z]
+ statement1.limit = 42
+ statement1.key = "zomg"
+ statement2 = Arel::Nodes::UpdateStatement.new
+ statement2.relation = "zomg"
+ statement2.wheres = 2
+ statement2.values = false
+ statement2.orders = %w[x y z]
+ statement2.limit = 42
+ statement2.key = "zomg"
+ array = [statement1, statement2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ statement1 = Arel::Nodes::UpdateStatement.new
+ statement1.relation = "zomg"
+ statement1.wheres = 2
+ statement1.values = false
+ statement1.orders = %w[x y z]
+ statement1.limit = 42
+ statement1.key = "zomg"
+ statement2 = Arel::Nodes::UpdateStatement.new
+ statement2.relation = "zomg"
+ statement2.wheres = 2
+ statement2.values = false
+ statement2.orders = %w[x y z]
+ statement2.limit = 42
+ statement2.key = "wth"
+ array = [statement1, statement2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes/window_test.rb b/activerecord/test/cases/arel/nodes/window_test.rb
new file mode 100644
index 0000000000..729b0556a4
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/window_test.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ describe "Window" do
+ describe "equality" do
+ it "is equal with equal ivars" do
+ window1 = Window.new
+ window1.orders = [1, 2]
+ window1.partitions = [1]
+ window1.frame 3
+ window2 = Window.new
+ window2.orders = [1, 2]
+ window2.partitions = [1]
+ window2.frame 3
+ array = [window1, window2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ window1 = Window.new
+ window1.orders = [1, 2]
+ window1.partitions = [1]
+ window1.frame 3
+ window2 = Window.new
+ window2.orders = [1, 2]
+ window1.partitions = [1]
+ window2.frame 4
+ array = [window1, window2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+
+ describe "NamedWindow" do
+ describe "equality" do
+ it "is equal with equal ivars" do
+ window1 = NamedWindow.new "foo"
+ window1.orders = [1, 2]
+ window1.partitions = [1]
+ window1.frame 3
+ window2 = NamedWindow.new "foo"
+ window2.orders = [1, 2]
+ window2.partitions = [1]
+ window2.frame 3
+ array = [window1, window2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ window1 = NamedWindow.new "foo"
+ window1.orders = [1, 2]
+ window1.partitions = [1]
+ window1.frame 3
+ window2 = NamedWindow.new "bar"
+ window2.orders = [1, 2]
+ window2.partitions = [1]
+ window2.frame 3
+ array = [window1, window2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+
+ describe "CurrentRow" do
+ describe "equality" do
+ it "is equal to other current row nodes" do
+ array = [CurrentRow.new, CurrentRow.new]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with other nodes" do
+ array = [CurrentRow.new, Node.new]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/nodes_test.rb b/activerecord/test/cases/arel/nodes_test.rb
new file mode 100644
index 0000000000..9021de0d20
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes_test.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ module Nodes
+ class TestNodes < Arel::Test
+ def test_every_arel_nodes_have_hash_eql_eqeq_from_same_class
+ # #descendants code from activesupport
+ node_descendants = []
+ ObjectSpace.each_object(Arel::Nodes::Node.singleton_class) do |k|
+ next if k.respond_to?(:singleton_class?) && k.singleton_class?
+ node_descendants.unshift k unless k == self
+ end
+ node_descendants.delete(Arel::Nodes::Node)
+ node_descendants.delete(Arel::Nodes::NodeExpression)
+
+ bad_node_descendants = node_descendants.reject do |subnode|
+ eqeq_owner = subnode.instance_method(:==).owner
+ eql_owner = subnode.instance_method(:eql?).owner
+ hash_owner = subnode.instance_method(:hash).owner
+
+ eqeq_owner < Arel::Nodes::Node &&
+ eqeq_owner == eql_owner &&
+ eqeq_owner == hash_owner
+ end
+
+ problem_msg = "Some subclasses of Arel::Nodes::Node do not have a" \
+ " #== or #eql? or #hash defined from the same class as the others"
+ assert_empty bad_node_descendants, problem_msg
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/select_manager_test.rb b/activerecord/test/cases/arel/select_manager_test.rb
new file mode 100644
index 0000000000..e6c49cd429
--- /dev/null
+++ b/activerecord/test/cases/arel/select_manager_test.rb
@@ -0,0 +1,1248 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class SelectManagerTest < Arel::Spec
+ def test_join_sources
+ manager = Arel::SelectManager.new
+ manager.join_sources << Arel::Nodes::StringJoin.new(Nodes.build_quoted("foo"))
+ assert_equal "SELECT FROM 'foo'", manager.to_sql
+ end
+
+ describe "backwards compatibility" do
+ describe "project" do
+ it "accepts symbols as sql literals" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.project :id
+ manager.from table
+ manager.to_sql.must_be_like %{
+ SELECT id FROM "users"
+ }
+ end
+ end
+
+ describe "order" do
+ it "accepts symbols" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.from table
+ manager.order :foo
+ manager.to_sql.must_be_like %{ SELECT * FROM "users" ORDER BY foo }
+ end
+ end
+
+ describe "group" do
+ it "takes a symbol" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.group :foo
+ manager.to_sql.must_be_like %{ SELECT FROM "users" GROUP BY foo }
+ end
+ end
+
+ describe "as" do
+ it "makes an AS node by grouping the AST" do
+ manager = Arel::SelectManager.new
+ as = manager.as(Arel.sql("foo"))
+ assert_kind_of Arel::Nodes::Grouping, as.left
+ assert_equal manager.ast, as.left.expr
+ assert_equal "foo", as.right
+ end
+
+ it "converts right to SqlLiteral if a string" do
+ manager = Arel::SelectManager.new
+ as = manager.as("foo")
+ assert_kind_of Arel::Nodes::SqlLiteral, as.right
+ end
+
+ it "can make a subselect" do
+ manager = Arel::SelectManager.new
+ manager.project Arel.star
+ manager.from Arel.sql("zomg")
+ as = manager.as(Arel.sql("foo"))
+
+ manager = Arel::SelectManager.new
+ manager.project Arel.sql("name")
+ manager.from as
+ manager.to_sql.must_be_like "SELECT name FROM (SELECT * FROM zomg) foo"
+ end
+ end
+
+ describe "from" do
+ it "ignores strings when table of same name exists" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+
+ manager.from table
+ manager.from "users"
+ manager.project table["id"]
+ manager.to_sql.must_be_like 'SELECT "users"."id" FROM users'
+ end
+
+ it "should support any ast" do
+ table = Table.new :users
+ manager1 = Arel::SelectManager.new
+
+ manager2 = Arel::SelectManager.new
+ manager2.project(Arel.sql("*"))
+ manager2.from table
+
+ manager1.project Arel.sql("lol")
+ as = manager2.as Arel.sql("omg")
+ manager1.from(as)
+
+ manager1.to_sql.must_be_like %{
+ SELECT lol FROM (SELECT * FROM "users") omg
+ }
+ end
+ end
+
+ describe "having" do
+ it "converts strings to SQLLiterals" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.having Arel.sql("foo")
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" HAVING foo }
+ end
+
+ it "can have multiple items specified separately" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.having Arel.sql("foo")
+ mgr.having Arel.sql("bar")
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" HAVING foo AND bar }
+ end
+
+ it "can receive any node" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.having Arel::Nodes::And.new([Arel.sql("foo"), Arel.sql("bar")])
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" HAVING foo AND bar }
+ end
+ end
+
+ describe "on" do
+ it "converts to sqlliterals" do
+ table = Table.new :users
+ right = table.alias
+ mgr = table.from
+ mgr.join(right).on("omg")
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" INNER JOIN "users" "users_2" ON omg }
+ end
+
+ it "converts to sqlliterals with multiple items" do
+ table = Table.new :users
+ right = table.alias
+ mgr = table.from
+ mgr.join(right).on("omg", "123")
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" INNER JOIN "users" "users_2" ON omg AND 123 }
+ end
+ end
+ end
+
+ describe "clone" do
+ it "creates new cores" do
+ table = Table.new :users, as: "foo"
+ mgr = table.from
+ m2 = mgr.clone
+ m2.project "foo"
+ mgr.to_sql.wont_equal m2.to_sql
+ end
+
+ it "makes updates to the correct copy" do
+ table = Table.new :users, as: "foo"
+ mgr = table.from
+ m2 = mgr.clone
+ m3 = m2.clone
+ m2.project "foo"
+ mgr.to_sql.wont_equal m2.to_sql
+ m3.to_sql.must_equal mgr.to_sql
+ end
+ end
+
+ describe "initialize" do
+ it "uses alias in sql" do
+ table = Table.new :users, as: "foo"
+ mgr = table.from
+ mgr.skip 10
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" "foo" OFFSET 10 }
+ end
+ end
+
+ describe "skip" do
+ it "should add an offset" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.skip 10
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" OFFSET 10 }
+ end
+
+ it "should chain" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.skip(10).to_sql.must_be_like %{ SELECT FROM "users" OFFSET 10 }
+ end
+ end
+
+ describe "offset" do
+ it "should add an offset" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.offset = 10
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" OFFSET 10 }
+ end
+
+ it "should remove an offset" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.offset = 10
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" OFFSET 10 }
+
+ mgr.offset = nil
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" }
+ end
+
+ it "should return the offset" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.offset = 10
+ assert_equal 10, mgr.offset
+ end
+ end
+
+ describe "exists" do
+ it "should create an exists clause" do
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.project Nodes::SqlLiteral.new "*"
+ m2 = Arel::SelectManager.new
+ m2.project manager.exists
+ m2.to_sql.must_be_like %{ SELECT EXISTS (#{manager.to_sql}) }
+ end
+
+ it "can be aliased" do
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.project Nodes::SqlLiteral.new "*"
+ m2 = Arel::SelectManager.new
+ m2.project manager.exists.as("foo")
+ m2.to_sql.must_be_like %{ SELECT EXISTS (#{manager.to_sql}) AS foo }
+ end
+ end
+
+ describe "union" do
+ before do
+ table = Table.new :users
+ @m1 = Arel::SelectManager.new table
+ @m1.project Arel.star
+ @m1.where(table[:age].lt(18))
+
+ @m2 = Arel::SelectManager.new table
+ @m2.project Arel.star
+ @m2.where(table[:age].gt(99))
+ end
+
+ it "should union two managers" do
+ # FIXME should this union "managers" or "statements" ?
+ # FIXME this probably shouldn't return a node
+ node = @m1.union @m2
+
+ # maybe FIXME: decide when wrapper parens are needed
+ node.to_sql.must_be_like %{
+ ( SELECT * FROM "users" WHERE "users"."age" < 18 UNION SELECT * FROM "users" WHERE "users"."age" > 99 )
+ }
+ end
+
+ it "should union all" do
+ node = @m1.union :all, @m2
+
+ node.to_sql.must_be_like %{
+ ( SELECT * FROM "users" WHERE "users"."age" < 18 UNION ALL SELECT * FROM "users" WHERE "users"."age" > 99 )
+ }
+ end
+ end
+
+ describe "intersect" do
+ before do
+ table = Table.new :users
+ @m1 = Arel::SelectManager.new table
+ @m1.project Arel.star
+ @m1.where(table[:age].gt(18))
+
+ @m2 = Arel::SelectManager.new table
+ @m2.project Arel.star
+ @m2.where(table[:age].lt(99))
+ end
+
+ it "should intersect two managers" do
+ # FIXME should this intersect "managers" or "statements" ?
+ # FIXME this probably shouldn't return a node
+ node = @m1.intersect @m2
+
+ # maybe FIXME: decide when wrapper parens are needed
+ node.to_sql.must_be_like %{
+ ( SELECT * FROM "users" WHERE "users"."age" > 18 INTERSECT SELECT * FROM "users" WHERE "users"."age" < 99 )
+ }
+ end
+ end
+
+ describe "except" do
+ before do
+ table = Table.new :users
+ @m1 = Arel::SelectManager.new table
+ @m1.project Arel.star
+ @m1.where(table[:age].between(18..60))
+
+ @m2 = Arel::SelectManager.new table
+ @m2.project Arel.star
+ @m2.where(table[:age].between(40..99))
+ end
+
+ it "should except two managers" do
+ # FIXME should this except "managers" or "statements" ?
+ # FIXME this probably shouldn't return a node
+ node = @m1.except @m2
+
+ # maybe FIXME: decide when wrapper parens are needed
+ node.to_sql.must_be_like %{
+ ( SELECT * FROM "users" WHERE "users"."age" BETWEEN 18 AND 60 EXCEPT SELECT * FROM "users" WHERE "users"."age" BETWEEN 40 AND 99 )
+ }
+ end
+ end
+
+ describe "with" do
+ it "should support basic WITH" do
+ users = Table.new(:users)
+ users_top = Table.new(:users_top)
+ comments = Table.new(:comments)
+
+ top = users.project(users[:id]).where(users[:karma].gt(100))
+ users_as = Arel::Nodes::As.new(users_top, top)
+ select_manager = comments.project(Arel.star).with(users_as)
+ .where(comments[:author_id].in(users_top.project(users_top[:id])))
+
+ select_manager.to_sql.must_be_like %{
+ WITH "users_top" AS (SELECT "users"."id" FROM "users" WHERE "users"."karma" > 100) SELECT * FROM "comments" WHERE "comments"."author_id" IN (SELECT "users_top"."id" FROM "users_top")
+ }
+ end
+
+ it "should support WITH RECURSIVE" do
+ comments = Table.new(:comments)
+ comments_id = comments[:id]
+ comments_parent_id = comments[:parent_id]
+
+ replies = Table.new(:replies)
+ replies_id = replies[:id]
+
+ recursive_term = Arel::SelectManager.new
+ recursive_term.from(comments).project(comments_id, comments_parent_id).where(comments_id.eq 42)
+
+ non_recursive_term = Arel::SelectManager.new
+ non_recursive_term.from(comments).project(comments_id, comments_parent_id).join(replies).on(comments_parent_id.eq replies_id)
+
+ union = recursive_term.union(non_recursive_term)
+
+ as_statement = Arel::Nodes::As.new replies, union
+
+ manager = Arel::SelectManager.new
+ manager.with(:recursive, as_statement).from(replies).project(Arel.star)
+
+ sql = manager.to_sql
+ sql.must_be_like %{
+ WITH RECURSIVE "replies" AS (
+ SELECT "comments"."id", "comments"."parent_id" FROM "comments" WHERE "comments"."id" = 42
+ UNION
+ SELECT "comments"."id", "comments"."parent_id" FROM "comments" INNER JOIN "replies" ON "comments"."parent_id" = "replies"."id"
+ )
+ SELECT * FROM "replies"
+ }
+ end
+ end
+
+ describe "ast" do
+ it "should return the ast" do
+ table = Table.new :users
+ mgr = table.from
+ assert mgr.ast
+ end
+
+ it "should allow orders to work when the ast is grepped" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.project Arel.sql "*"
+ mgr.from table
+ mgr.orders << Arel::Nodes::Ascending.new(Arel.sql("foo"))
+ mgr.ast.grep(Arel::Nodes::OuterJoin)
+ mgr.to_sql.must_be_like %{ SELECT * FROM "users" ORDER BY foo ASC }
+ end
+ end
+
+ describe "taken" do
+ it "should return limit" do
+ manager = Arel::SelectManager.new
+ manager.take 10
+ manager.taken.must_equal 10
+ end
+ end
+
+ describe "lock" do
+ # This should fail on other databases
+ it "adds a lock node" do
+ table = Table.new :users
+ mgr = table.from
+ mgr.lock.to_sql.must_be_like %{ SELECT FROM "users" FOR UPDATE }
+ end
+ end
+
+ describe "orders" do
+ it "returns order clauses" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ order = table[:id]
+ manager.order table[:id]
+ manager.orders.must_equal [order]
+ end
+ end
+
+ describe "order" do
+ it "generates order clauses" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.from table
+ manager.order table[:id]
+ manager.to_sql.must_be_like %{
+ SELECT * FROM "users" ORDER BY "users"."id"
+ }
+ end
+
+ # FIXME: I would like to deprecate this
+ it "takes *args" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.from table
+ manager.order table[:id], table[:name]
+ manager.to_sql.must_be_like %{
+ SELECT * FROM "users" ORDER BY "users"."id", "users"."name"
+ }
+ end
+
+ it "chains" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.order(table[:id]).must_equal manager
+ end
+
+ it "has order attributes" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.from table
+ manager.order table[:id].desc
+ manager.to_sql.must_be_like %{
+ SELECT * FROM "users" ORDER BY "users"."id" DESC
+ }
+ end
+ end
+
+ describe "on" do
+ it "takes two params" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right).on(predicate, predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ INNER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id" AND
+ "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "takes three params" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right).on(
+ predicate,
+ predicate,
+ left[:name].eq(right[:name])
+ )
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ INNER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id" AND
+ "users"."id" = "users_2"."id" AND
+ "users"."name" = "users_2"."name"
+ }
+ end
+ end
+
+ it "should hand back froms" do
+ relation = Arel::SelectManager.new
+ assert_equal [], relation.froms
+ end
+
+ it "should create and nodes" do
+ relation = Arel::SelectManager.new
+ children = ["foo", "bar", "baz"]
+ clause = relation.create_and children
+ assert_kind_of Arel::Nodes::And, clause
+ assert_equal children, clause.children
+ end
+
+ it "should create insert managers" do
+ relation = Arel::SelectManager.new
+ insert = relation.create_insert
+ assert_kind_of Arel::InsertManager, insert
+ end
+
+ it "should create join nodes" do
+ relation = Arel::SelectManager.new
+ join = relation.create_join "foo", "bar"
+ assert_kind_of Arel::Nodes::InnerJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a full outer join klass" do
+ relation = Arel::SelectManager.new
+ join = relation.create_join "foo", "bar", Arel::Nodes::FullOuterJoin
+ assert_kind_of Arel::Nodes::FullOuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a outer join klass" do
+ relation = Arel::SelectManager.new
+ join = relation.create_join "foo", "bar", Arel::Nodes::OuterJoin
+ assert_kind_of Arel::Nodes::OuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a right outer join klass" do
+ relation = Arel::SelectManager.new
+ join = relation.create_join "foo", "bar", Arel::Nodes::RightOuterJoin
+ assert_kind_of Arel::Nodes::RightOuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ describe "join" do
+ it "responds to join" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right).on(predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ INNER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "takes a class" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right, Nodes::OuterJoin).on(predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ LEFT OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "takes the full outer join class" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right, Nodes::FullOuterJoin).on(predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ FULL OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "takes the right outer join class" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.join(right, Nodes::RightOuterJoin).on(predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ RIGHT OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "noops on nil" do
+ manager = Arel::SelectManager.new
+ manager.join(nil).must_equal manager
+ end
+
+ it "raises EmptyJoinError on empty" do
+ left = Table.new :users
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ assert_raises(EmptyJoinError) do
+ manager.join("")
+ end
+ end
+ end
+
+ describe "outer join" do
+ it "responds to join" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+ manager = Arel::SelectManager.new
+
+ manager.from left
+ manager.outer_join(right).on(predicate)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users"
+ LEFT OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "noops on nil" do
+ manager = Arel::SelectManager.new
+ manager.outer_join(nil).must_equal manager
+ end
+ end
+
+ describe "joins" do
+ it "returns inner join sql" do
+ table = Table.new :users
+ aliaz = table.alias
+ manager = Arel::SelectManager.new
+ manager.from Nodes::InnerJoin.new(aliaz, table[:id].eq(aliaz[:id]))
+ assert_match 'INNER JOIN "users" "users_2" "users"."id" = "users_2"."id"',
+ manager.to_sql
+ end
+
+ it "returns outer join sql" do
+ table = Table.new :users
+ aliaz = table.alias
+ manager = Arel::SelectManager.new
+ manager.from Nodes::OuterJoin.new(aliaz, table[:id].eq(aliaz[:id]))
+ assert_match 'LEFT OUTER JOIN "users" "users_2" "users"."id" = "users_2"."id"',
+ manager.to_sql
+ end
+
+ it "can have a non-table alias as relation name" do
+ users = Table.new :users
+ comments = Table.new :comments
+
+ counts = comments.from.
+ group(comments[:user_id]).
+ project(
+ comments[:user_id].as("user_id"),
+ comments[:user_id].count.as("count")
+ ).as("counts")
+
+ joins = users.join(counts).on(counts[:user_id].eq(10))
+ joins.to_sql.must_be_like %{
+ SELECT FROM "users" INNER JOIN (SELECT "comments"."user_id" AS user_id, COUNT("comments"."user_id") AS count FROM "comments" GROUP BY "comments"."user_id") counts ON counts."user_id" = 10
+ }
+ end
+
+ it "joins itself" do
+ left = Table.new :users
+ right = left.alias
+ predicate = left[:id].eq(right[:id])
+
+ mgr = left.join(right)
+ mgr.project Nodes::SqlLiteral.new("*")
+ mgr.on(predicate).must_equal mgr
+
+ mgr.to_sql.must_be_like %{
+ SELECT * FROM "users"
+ INNER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+
+ it "returns string join sql" do
+ manager = Arel::SelectManager.new
+ manager.from Nodes::StringJoin.new(Nodes.build_quoted("hello"))
+ assert_match "'hello'", manager.to_sql
+ end
+ end
+
+ describe "group" do
+ it "takes an attribute" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.group table[:id]
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" GROUP BY "users"."id"
+ }
+ end
+
+ it "chains" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.group(table[:id]).must_equal manager
+ end
+
+ it "takes multiple args" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.group table[:id], table[:name]
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" GROUP BY "users"."id", "users"."name"
+ }
+ end
+
+ # FIXME: backwards compat
+ it "makes strings literals" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.group "foo"
+ manager.to_sql.must_be_like %{ SELECT FROM "users" GROUP BY foo }
+ end
+ end
+
+ describe "window definition" do
+ it "can be empty" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window")
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS ()
+ }
+ end
+
+ it "takes an order" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").order(table["foo"].asc)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ORDER BY "users"."foo" ASC)
+ }
+ end
+
+ it "takes an order with multiple columns" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").order(table["foo"].asc, table["bar"].desc)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ORDER BY "users"."foo" ASC, "users"."bar" DESC)
+ }
+ end
+
+ it "takes a partition" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").partition(table["bar"])
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (PARTITION BY "users"."bar")
+ }
+ end
+
+ it "takes a partition and an order" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").partition(table["foo"]).order(table["foo"].asc)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (PARTITION BY "users"."foo"
+ ORDER BY "users"."foo" ASC)
+ }
+ end
+
+ it "takes a partition with multiple columns" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").partition(table["bar"], table["baz"])
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (PARTITION BY "users"."bar", "users"."baz")
+ }
+ end
+
+ it "takes a rows frame, unbounded preceding" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").rows(Arel::Nodes::Preceding.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS UNBOUNDED PRECEDING)
+ }
+ end
+
+ it "takes a rows frame, bounded preceding" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").rows(Arel::Nodes::Preceding.new(5))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS 5 PRECEDING)
+ }
+ end
+
+ it "takes a rows frame, unbounded following" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").rows(Arel::Nodes::Following.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS UNBOUNDED FOLLOWING)
+ }
+ end
+
+ it "takes a rows frame, bounded following" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").rows(Arel::Nodes::Following.new(5))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS 5 FOLLOWING)
+ }
+ end
+
+ it "takes a rows frame, current row" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").rows(Arel::Nodes::CurrentRow.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS CURRENT ROW)
+ }
+ end
+
+ it "takes a rows frame, between two delimiters" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ window = manager.window("a_window")
+ window.frame(
+ Arel::Nodes::Between.new(
+ window.rows,
+ Nodes::And.new([
+ Arel::Nodes::Preceding.new,
+ Arel::Nodes::CurrentRow.new
+ ])))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
+ }
+ end
+
+ it "takes a range frame, unbounded preceding" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").range(Arel::Nodes::Preceding.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE UNBOUNDED PRECEDING)
+ }
+ end
+
+ it "takes a range frame, bounded preceding" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").range(Arel::Nodes::Preceding.new(5))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE 5 PRECEDING)
+ }
+ end
+
+ it "takes a range frame, unbounded following" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").range(Arel::Nodes::Following.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE UNBOUNDED FOLLOWING)
+ }
+ end
+
+ it "takes a range frame, bounded following" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").range(Arel::Nodes::Following.new(5))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE 5 FOLLOWING)
+ }
+ end
+
+ it "takes a range frame, current row" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.window("a_window").range(Arel::Nodes::CurrentRow.new)
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE CURRENT ROW)
+ }
+ end
+
+ it "takes a range frame, between two delimiters" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ window = manager.window("a_window")
+ window.frame(
+ Arel::Nodes::Between.new(
+ window.range,
+ Nodes::And.new([
+ Arel::Nodes::Preceding.new,
+ Arel::Nodes::CurrentRow.new
+ ])))
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" WINDOW "a_window" AS (RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
+ }
+ end
+ end
+
+ describe "delete" do
+ it "copies from" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ stmt = manager.compile_delete
+
+ stmt.to_sql.must_be_like %{ DELETE FROM "users" }
+ end
+
+ it "copies where" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.where table[:id].eq 10
+ stmt = manager.compile_delete
+
+ stmt.to_sql.must_be_like %{
+ DELETE FROM "users" WHERE "users"."id" = 10
+ }
+ end
+ end
+
+ describe "where_sql" do
+ it "gives me back the where sql" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.where table[:id].eq 10
+ manager.where_sql.must_be_like %{ WHERE "users"."id" = 10 }
+ end
+
+ it "joins wheres with AND" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.where table[:id].eq 10
+ manager.where table[:id].eq 11
+ manager.where_sql.must_be_like %{ WHERE "users"."id" = 10 AND "users"."id" = 11}
+ end
+
+ it "handles database specific statements" do
+ old_visitor = Table.engine.connection.visitor
+ Table.engine.connection.visitor = Visitors::PostgreSQL.new Table.engine.connection
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.where table[:id].eq 10
+ manager.where table[:name].matches "foo%"
+ manager.where_sql.must_be_like %{ WHERE "users"."id" = 10 AND "users"."name" ILIKE 'foo%' }
+ Table.engine.connection.visitor = old_visitor
+ end
+
+ it "returns nil when there are no wheres" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.where_sql.must_be_nil
+ end
+ end
+
+ describe "update" do
+ it "creates an update statement" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ stmt = manager.compile_update({ table[:id] => 1 }, Arel::Attributes::Attribute.new(table, "id"))
+
+ stmt.to_sql.must_be_like %{
+ UPDATE "users" SET "id" = 1
+ }
+ end
+
+ it "takes a string" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ stmt = manager.compile_update(Nodes::SqlLiteral.new("foo = bar"), Arel::Attributes::Attribute.new(table, "id"))
+
+ stmt.to_sql.must_be_like %{ UPDATE "users" SET foo = bar }
+ end
+
+ it "copies limits" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.take 1
+ stmt = manager.compile_update(Nodes::SqlLiteral.new("foo = bar"), Arel::Attributes::Attribute.new(table, "id"))
+ stmt.key = table["id"]
+
+ stmt.to_sql.must_be_like %{
+ UPDATE "users" SET foo = bar
+ WHERE "users"."id" IN (SELECT "users"."id" FROM "users" LIMIT 1)
+ }
+ end
+
+ it "copies order" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from table
+ manager.order :foo
+ stmt = manager.compile_update(Nodes::SqlLiteral.new("foo = bar"), Arel::Attributes::Attribute.new(table, "id"))
+ stmt.key = table["id"]
+
+ stmt.to_sql.must_be_like %{
+ UPDATE "users" SET foo = bar
+ WHERE "users"."id" IN (SELECT "users"."id" FROM "users" ORDER BY foo)
+ }
+ end
+
+ it "copies where clauses" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.where table[:id].eq 10
+ manager.from table
+ stmt = manager.compile_update({ table[:id] => 1 }, Arel::Attributes::Attribute.new(table, "id"))
+
+ stmt.to_sql.must_be_like %{
+ UPDATE "users" SET "id" = 1 WHERE "users"."id" = 10
+ }
+ end
+
+ it "copies where clauses when nesting is triggered" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.where table[:foo].eq 10
+ manager.take 42
+ manager.from table
+ stmt = manager.compile_update({ table[:id] => 1 }, Arel::Attributes::Attribute.new(table, "id"))
+
+ stmt.to_sql.must_be_like %{
+ UPDATE "users" SET "id" = 1 WHERE "users"."id" IN (SELECT "users"."id" FROM "users" WHERE "users"."foo" = 10 LIMIT 42)
+ }
+ end
+ end
+
+ describe "project" do
+ it "takes sql literals" do
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.to_sql.must_be_like %{ SELECT * }
+ end
+
+ it "takes multiple args" do
+ manager = Arel::SelectManager.new
+ manager.project Nodes::SqlLiteral.new("foo"),
+ Nodes::SqlLiteral.new("bar")
+ manager.to_sql.must_be_like %{ SELECT foo, bar }
+ end
+
+ it "takes strings" do
+ manager = Arel::SelectManager.new
+ manager.project "*"
+ manager.to_sql.must_be_like %{ SELECT * }
+ end
+ end
+
+ describe "projections" do
+ it "reads projections" do
+ manager = Arel::SelectManager.new
+ manager.project Arel.sql("foo"), Arel.sql("bar")
+ manager.projections.must_equal [Arel.sql("foo"), Arel.sql("bar")]
+ end
+ end
+
+ describe "projections=" do
+ it "overwrites projections" do
+ manager = Arel::SelectManager.new
+ manager.project Arel.sql("foo")
+ manager.projections = [Arel.sql("bar")]
+ manager.to_sql.must_be_like %{ SELECT bar }
+ end
+ end
+
+ describe "take" do
+ it "knows take" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from(table).project(table["id"])
+ manager.where(table["id"].eq(1))
+ manager.take 1
+
+ manager.to_sql.must_be_like %{
+ SELECT "users"."id"
+ FROM "users"
+ WHERE "users"."id" = 1
+ LIMIT 1
+ }
+ end
+
+ it "chains" do
+ manager = Arel::SelectManager.new
+ manager.take(1).must_equal manager
+ end
+
+ it "removes LIMIT when nil is passed" do
+ manager = Arel::SelectManager.new
+ manager.limit = 10
+ assert_match("LIMIT", manager.to_sql)
+
+ manager.limit = nil
+ assert_no_match("LIMIT", manager.to_sql)
+ end
+ end
+
+ describe "where" do
+ it "knows where" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from(table).project(table["id"])
+ manager.where(table["id"].eq(1))
+ manager.to_sql.must_be_like %{
+ SELECT "users"."id"
+ FROM "users"
+ WHERE "users"."id" = 1
+ }
+ end
+
+ it "chains" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from(table)
+ manager.project(table["id"]).where(table["id"].eq 1).must_equal manager
+ end
+ end
+
+ describe "from" do
+ it "makes sql" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+
+ manager.from table
+ manager.project table["id"]
+ manager.to_sql.must_be_like 'SELECT "users"."id" FROM "users"'
+ end
+
+ it "chains" do
+ table = Table.new :users
+ manager = Arel::SelectManager.new
+ manager.from(table).project(table["id"]).must_equal manager
+ manager.to_sql.must_be_like 'SELECT "users"."id" FROM "users"'
+ end
+ end
+
+ describe "source" do
+ it "returns the join source of the select core" do
+ manager = Arel::SelectManager.new
+ manager.source.must_equal manager.ast.cores.last.source
+ end
+ end
+
+ describe "distinct" do
+ it "sets the quantifier" do
+ manager = Arel::SelectManager.new
+
+ manager.distinct
+ manager.ast.cores.last.set_quantifier.class.must_equal Arel::Nodes::Distinct
+
+ manager.distinct(false)
+ manager.ast.cores.last.set_quantifier.must_be_nil
+ end
+
+ it "chains" do
+ manager = Arel::SelectManager.new
+ manager.distinct.must_equal manager
+ manager.distinct(false).must_equal manager
+ end
+ end
+
+ describe "distinct_on" do
+ it "sets the quantifier" do
+ manager = Arel::SelectManager.new
+ table = Table.new :users
+
+ manager.distinct_on(table["id"])
+ manager.ast.cores.last.set_quantifier.must_equal Arel::Nodes::DistinctOn.new(table["id"])
+
+ manager.distinct_on(false)
+ manager.ast.cores.last.set_quantifier.must_be_nil
+ end
+
+ it "chains" do
+ manager = Arel::SelectManager.new
+ table = Table.new :users
+
+ manager.distinct_on(table["id"]).must_equal manager
+ manager.distinct_on(false).must_equal manager
+ end
+ end
+
+ describe "comment" do
+ it "chains" do
+ manager = Arel::SelectManager.new
+ manager.comment("selecting").must_equal manager
+ end
+
+ it "appends a comment to the generated query" do
+ manager = Arel::SelectManager.new
+ table = Table.new :users
+ manager.from(table).project(table["id"])
+
+ manager.comment("selecting")
+ manager.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" /* selecting */
+ }
+
+ manager.comment("selecting", "with", "comment")
+ manager.to_sql.must_be_like %{
+ SELECT "users"."id" FROM "users" /* selecting */ /* with */ /* comment */
+ }
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/support/fake_record.rb b/activerecord/test/cases/arel/support/fake_record.rb
new file mode 100644
index 0000000000..18e6c10c9d
--- /dev/null
+++ b/activerecord/test/cases/arel/support/fake_record.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+require "date"
+module FakeRecord
+ class Column < Struct.new(:name, :type)
+ end
+
+ class Connection
+ attr_reader :tables
+ attr_accessor :visitor
+
+ def initialize(visitor = nil)
+ @tables = %w{ users photos developers products}
+ @columns = {
+ "users" => [
+ Column.new("id", :integer),
+ Column.new("name", :string),
+ Column.new("bool", :boolean),
+ Column.new("created_at", :date)
+ ],
+ "products" => [
+ Column.new("id", :integer),
+ Column.new("price", :decimal)
+ ]
+ }
+ @columns_hash = {
+ "users" => Hash[@columns["users"].map { |x| [x.name, x] }],
+ "products" => Hash[@columns["products"].map { |x| [x.name, x] }]
+ }
+ @primary_keys = {
+ "users" => "id",
+ "products" => "id"
+ }
+ @visitor = visitor
+ end
+
+ def columns_hash(table_name)
+ @columns_hash[table_name]
+ end
+
+ def primary_key(name)
+ @primary_keys[name.to_s]
+ end
+
+ def data_source_exists?(name)
+ @tables.include? name.to_s
+ end
+
+ def columns(name, message = nil)
+ @columns[name.to_s]
+ end
+
+ def quote_table_name(name)
+ "\"#{name}\""
+ end
+
+ def quote_column_name(name)
+ "\"#{name}\""
+ end
+
+ def sanitize_as_sql_comment(comment)
+ comment
+ end
+
+ def schema_cache
+ self
+ end
+
+ def quote(thing)
+ case thing
+ when DateTime
+ "'#{thing.strftime("%Y-%m-%d %H:%M:%S")}'"
+ when Date
+ "'#{thing.strftime("%Y-%m-%d")}'"
+ when true
+ "'t'"
+ when false
+ "'f'"
+ when nil
+ "NULL"
+ when Numeric
+ thing
+ else
+ "'#{thing.to_s.gsub("'", "\\\\'")}'"
+ end
+ end
+ end
+
+ class ConnectionPool
+ class Spec < Struct.new(:config)
+ end
+
+ attr_reader :spec, :connection
+
+ def initialize
+ @spec = Spec.new(adapter: "america")
+ @connection = Connection.new
+ @connection.visitor = Arel::Visitors::ToSql.new(connection)
+ end
+
+ def with_connection
+ yield connection
+ end
+
+ def table_exists?(name)
+ connection.tables.include? name.to_s
+ end
+
+ def columns_hash
+ connection.columns_hash
+ end
+
+ def schema_cache
+ connection
+ end
+
+ def quote(thing)
+ connection.quote thing
+ end
+ end
+
+ class Base
+ attr_accessor :connection_pool
+
+ def initialize
+ @connection_pool = ConnectionPool.new
+ end
+
+ def connection
+ connection_pool.connection
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/table_test.rb b/activerecord/test/cases/arel/table_test.rb
new file mode 100644
index 0000000000..91b7a5a480
--- /dev/null
+++ b/activerecord/test/cases/arel/table_test.rb
@@ -0,0 +1,216 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class TableTest < Arel::Spec
+ before do
+ @relation = Table.new(:users)
+ end
+
+ it "should create join nodes" do
+ join = @relation.create_string_join "foo"
+ assert_kind_of Arel::Nodes::StringJoin, join
+ assert_equal "foo", join.left
+ end
+
+ it "should create join nodes" do
+ join = @relation.create_join "foo", "bar"
+ assert_kind_of Arel::Nodes::InnerJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a klass" do
+ join = @relation.create_join "foo", "bar", Arel::Nodes::FullOuterJoin
+ assert_kind_of Arel::Nodes::FullOuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a klass" do
+ join = @relation.create_join "foo", "bar", Arel::Nodes::OuterJoin
+ assert_kind_of Arel::Nodes::OuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should create join nodes with a klass" do
+ join = @relation.create_join "foo", "bar", Arel::Nodes::RightOuterJoin
+ assert_kind_of Arel::Nodes::RightOuterJoin, join
+ assert_equal "foo", join.left
+ assert_equal "bar", join.right
+ end
+
+ it "should return an insert manager" do
+ im = @relation.compile_insert "VALUES(NULL)"
+ assert_kind_of Arel::InsertManager, im
+ im.into Table.new(:users)
+ assert_equal "INSERT INTO \"users\" VALUES(NULL)", im.to_sql
+ end
+
+ describe "skip" do
+ it "should add an offset" do
+ sm = @relation.skip 2
+ sm.to_sql.must_be_like "SELECT FROM \"users\" OFFSET 2"
+ end
+ end
+
+ describe "having" do
+ it "adds a having clause" do
+ mgr = @relation.having @relation[:id].eq(10)
+ mgr.to_sql.must_be_like %{
+ SELECT FROM "users" HAVING "users"."id" = 10
+ }
+ end
+ end
+
+ describe "backwards compat" do
+ describe "join" do
+ it "noops on nil" do
+ mgr = @relation.join nil
+
+ mgr.to_sql.must_be_like %{ SELECT FROM "users" }
+ end
+
+ it "raises EmptyJoinError on empty" do
+ assert_raises(EmptyJoinError) do
+ @relation.join ""
+ end
+ end
+
+ it "takes a second argument for join type" do
+ right = @relation.alias
+ predicate = @relation[:id].eq(right[:id])
+ mgr = @relation.join(right, Nodes::OuterJoin).on(predicate)
+
+ mgr.to_sql.must_be_like %{
+ SELECT FROM "users"
+ LEFT OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+ end
+
+ describe "join" do
+ it "creates an outer join" do
+ right = @relation.alias
+ predicate = @relation[:id].eq(right[:id])
+ mgr = @relation.outer_join(right).on(predicate)
+
+ mgr.to_sql.must_be_like %{
+ SELECT FROM "users"
+ LEFT OUTER JOIN "users" "users_2"
+ ON "users"."id" = "users_2"."id"
+ }
+ end
+ end
+ end
+
+ describe "group" do
+ it "should create a group" do
+ manager = @relation.group @relation[:id]
+ manager.to_sql.must_be_like %{
+ SELECT FROM "users" GROUP BY "users"."id"
+ }
+ end
+ end
+
+ describe "alias" do
+ it "should create a node that proxies to a table" do
+ node = @relation.alias
+ node.name.must_equal "users_2"
+ node[:id].relation.must_equal node
+ end
+ end
+
+ describe "new" do
+ it "should accept a hash" do
+ rel = Table.new :users, as: "foo"
+ rel.table_alias.must_equal "foo"
+ end
+
+ it "ignores as if it equals name" do
+ rel = Table.new :users, as: "users"
+ rel.table_alias.must_be_nil
+ end
+ end
+
+ describe "order" do
+ it "should take an order" do
+ manager = @relation.order "foo"
+ manager.to_sql.must_be_like %{ SELECT FROM "users" ORDER BY foo }
+ end
+ end
+
+ describe "take" do
+ it "should add a limit" do
+ manager = @relation.take 1
+ manager.project Nodes::SqlLiteral.new "*"
+ manager.to_sql.must_be_like %{ SELECT * FROM "users" LIMIT 1 }
+ end
+ end
+
+ describe "project" do
+ it "can project" do
+ manager = @relation.project Nodes::SqlLiteral.new "*"
+ manager.to_sql.must_be_like %{ SELECT * FROM "users" }
+ end
+
+ it "takes multiple parameters" do
+ manager = @relation.project Nodes::SqlLiteral.new("*"), Nodes::SqlLiteral.new("*")
+ manager.to_sql.must_be_like %{ SELECT *, * FROM "users" }
+ end
+ end
+
+ describe "where" do
+ it "returns a tree manager" do
+ manager = @relation.where @relation[:id].eq 1
+ manager.project @relation[:id]
+ manager.must_be_kind_of TreeManager
+ manager.to_sql.must_be_like %{
+ SELECT "users"."id"
+ FROM "users"
+ WHERE "users"."id" = 1
+ }
+ end
+ end
+
+ it "should have a name" do
+ @relation.name.must_equal "users"
+ end
+
+ it "should have a table name" do
+ @relation.table_name.must_equal "users"
+ end
+
+ describe "[]" do
+ describe "when given a Symbol" do
+ it "manufactures an attribute if the symbol names an attribute within the relation" do
+ column = @relation[:id]
+ column.name.must_equal :id
+ end
+ end
+ end
+
+ describe "equality" do
+ it "is equal with equal ivars" do
+ relation1 = Table.new(:users)
+ relation1.table_alias = "zomg"
+ relation2 = Table.new(:users)
+ relation2.table_alias = "zomg"
+ array = [relation1, relation2]
+ assert_equal 1, array.uniq.size
+ end
+
+ it "is not equal with different ivars" do
+ relation1 = Table.new(:users)
+ relation1.table_alias = "zomg"
+ relation2 = Table.new(:users)
+ relation2.table_alias = "zomg2"
+ array = [relation1, relation2]
+ assert_equal 2, array.uniq.size
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/update_manager_test.rb b/activerecord/test/cases/arel/update_manager_test.rb
new file mode 100644
index 0000000000..cc1b9ac5b3
--- /dev/null
+++ b/activerecord/test/cases/arel/update_manager_test.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+require_relative "helper"
+
+module Arel
+ class UpdateManagerTest < Arel::Spec
+ describe "new" do
+ it "takes an engine" do
+ Arel::UpdateManager.new
+ end
+ end
+
+ it "should not quote sql literals" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.table table
+ um.set [[table[:name], Arel::Nodes::BindParam.new(1)]]
+ um.to_sql.must_be_like %{ UPDATE "users" SET "name" = ? }
+ end
+
+ it "handles limit properly" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.key = "id"
+ um.take 10
+ um.table table
+ um.set [[table[:name], nil]]
+ assert_match(/LIMIT 10/, um.to_sql)
+ end
+
+ describe "set" do
+ it "updates with null" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.table table
+ um.set [[table[:name], nil]]
+ um.to_sql.must_be_like %{ UPDATE "users" SET "name" = NULL }
+ end
+
+ it "takes a string" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.table table
+ um.set Nodes::SqlLiteral.new "foo = bar"
+ um.to_sql.must_be_like %{ UPDATE "users" SET foo = bar }
+ end
+
+ it "takes a list of lists" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.table table
+ um.set [[table[:id], 1], [table[:name], "hello"]]
+ um.to_sql.must_be_like %{
+ UPDATE "users" SET "id" = 1, "name" = 'hello'
+ }
+ end
+
+ it "chains" do
+ table = Table.new(:users)
+ um = Arel::UpdateManager.new
+ um.set([[table[:id], 1], [table[:name], "hello"]]).must_equal um
+ end
+ end
+
+ describe "table" do
+ it "generates an update statement" do
+ um = Arel::UpdateManager.new
+ um.table Table.new(:users)
+ um.to_sql.must_be_like %{ UPDATE "users" }
+ end
+
+ it "chains" do
+ um = Arel::UpdateManager.new
+ um.table(Table.new(:users)).must_equal um
+ end
+
+ it "generates an update statement with joins" do
+ um = Arel::UpdateManager.new
+
+ table = Table.new(:users)
+ join_source = Arel::Nodes::JoinSource.new(
+ table,
+ [table.create_join(Table.new(:posts))]
+ )
+
+ um.table join_source
+ um.to_sql.must_be_like %{ UPDATE "users" INNER JOIN "posts" }
+ end
+ end
+
+ describe "where" do
+ it "generates a where clause" do
+ table = Table.new :users
+ um = Arel::UpdateManager.new
+ um.table table
+ um.where table[:id].eq(1)
+ um.to_sql.must_be_like %{
+ UPDATE "users" WHERE "users"."id" = 1
+ }
+ end
+
+ it "chains" do
+ table = Table.new :users
+ um = Arel::UpdateManager.new
+ um.table table
+ um.where(table[:id].eq(1)).must_equal um
+ end
+ end
+
+ describe "key" do
+ before do
+ @table = Table.new :users
+ @um = Arel::UpdateManager.new
+ @um.key = @table[:foo]
+ end
+
+ it "can be set" do
+ @um.ast.key.must_equal @table[:foo]
+ end
+
+ it "can be accessed" do
+ @um.key.must_equal @table[:foo]
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/depth_first_test.rb b/activerecord/test/cases/arel/visitors/depth_first_test.rb
new file mode 100644
index 0000000000..106be2311d
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/depth_first_test.rb
@@ -0,0 +1,276 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class TestDepthFirst < Arel::Test
+ Collector = Struct.new(:calls) do
+ def call(object)
+ calls << object
+ end
+ end
+
+ def setup
+ @collector = Collector.new []
+ @visitor = Visitors::DepthFirst.new @collector
+ end
+
+ def test_raises_with_object
+ assert_raises(TypeError) do
+ @visitor.accept(Object.new)
+ end
+ end
+
+
+ # unary ops
+ [
+ Arel::Nodes::Not,
+ Arel::Nodes::Group,
+ Arel::Nodes::On,
+ Arel::Nodes::Grouping,
+ Arel::Nodes::Offset,
+ Arel::Nodes::Ordering,
+ Arel::Nodes::StringJoin,
+ Arel::Nodes::UnqualifiedColumn,
+ Arel::Nodes::ValuesList,
+ Arel::Nodes::Limit,
+ Arel::Nodes::Else,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ op = klass.new(:a)
+ @visitor.accept op
+ assert_equal [:a, op], @collector.calls
+ end
+ end
+
+ # functions
+ [
+ Arel::Nodes::Exists,
+ Arel::Nodes::Avg,
+ Arel::Nodes::Min,
+ Arel::Nodes::Max,
+ Arel::Nodes::Sum,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ func = klass.new(:a, "b")
+ @visitor.accept func
+ assert_equal [:a, "b", false, func], @collector.calls
+ end
+ end
+
+ def test_named_function
+ func = Arel::Nodes::NamedFunction.new(:a, :b, "c")
+ @visitor.accept func
+ assert_equal [:a, :b, false, "c", func], @collector.calls
+ end
+
+ def test_lock
+ lock = Nodes::Lock.new true
+ @visitor.accept lock
+ assert_equal [lock], @collector.calls
+ end
+
+ def test_count
+ count = Nodes::Count.new :a, :b, "c"
+ @visitor.accept count
+ assert_equal [:a, "c", :b, count], @collector.calls
+ end
+
+ def test_inner_join
+ join = Nodes::InnerJoin.new :a, :b
+ @visitor.accept join
+ assert_equal [:a, :b, join], @collector.calls
+ end
+
+ def test_full_outer_join
+ join = Nodes::FullOuterJoin.new :a, :b
+ @visitor.accept join
+ assert_equal [:a, :b, join], @collector.calls
+ end
+
+ def test_outer_join
+ join = Nodes::OuterJoin.new :a, :b
+ @visitor.accept join
+ assert_equal [:a, :b, join], @collector.calls
+ end
+
+ def test_right_outer_join
+ join = Nodes::RightOuterJoin.new :a, :b
+ @visitor.accept join
+ assert_equal [:a, :b, join], @collector.calls
+ end
+
+ def test_comment
+ comment = Nodes::Comment.new ["foo"]
+ @visitor.accept comment
+ assert_equal ["foo", ["foo"], comment], @collector.calls
+ end
+
+ [
+ Arel::Nodes::Assignment,
+ Arel::Nodes::Between,
+ Arel::Nodes::Concat,
+ Arel::Nodes::DoesNotMatch,
+ Arel::Nodes::Equality,
+ Arel::Nodes::GreaterThan,
+ Arel::Nodes::GreaterThanOrEqual,
+ Arel::Nodes::In,
+ Arel::Nodes::LessThan,
+ Arel::Nodes::LessThanOrEqual,
+ Arel::Nodes::Matches,
+ Arel::Nodes::NotEqual,
+ Arel::Nodes::NotIn,
+ Arel::Nodes::Or,
+ Arel::Nodes::TableAlias,
+ Arel::Nodes::As,
+ Arel::Nodes::DeleteStatement,
+ Arel::Nodes::JoinSource,
+ Arel::Nodes::When,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ binary = klass.new(:a, :b)
+ @visitor.accept binary
+ assert_equal [:a, :b, binary], @collector.calls
+ end
+ end
+
+ def test_Arel_Nodes_InfixOperation
+ binary = Arel::Nodes::InfixOperation.new(:o, :a, :b)
+ @visitor.accept binary
+ assert_equal [:a, :b, binary], @collector.calls
+ end
+
+ # N-ary
+ [
+ Arel::Nodes::And,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ binary = klass.new([:a, :b, :c])
+ @visitor.accept binary
+ assert_equal [:a, :b, :c, binary], @collector.calls
+ end
+ end
+
+ [
+ Arel::Attributes::Integer,
+ Arel::Attributes::Float,
+ Arel::Attributes::String,
+ Arel::Attributes::Time,
+ Arel::Attributes::Boolean,
+ Arel::Attributes::Attribute
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ binary = klass.new(:a, :b)
+ @visitor.accept binary
+ assert_equal [:a, :b, binary], @collector.calls
+ end
+ end
+
+ def test_table
+ relation = Arel::Table.new(:users)
+ @visitor.accept relation
+ assert_equal ["users", relation], @collector.calls
+ end
+
+ def test_array
+ node = Nodes::Or.new(:a, :b)
+ list = [node]
+ @visitor.accept list
+ assert_equal [:a, :b, node, list], @collector.calls
+ end
+
+ def test_set
+ node = Nodes::Or.new(:a, :b)
+ set = Set.new([node])
+ @visitor.accept set
+ assert_equal [:a, :b, node, set], @collector.calls
+ end
+
+ def test_hash
+ node = Nodes::Or.new(:a, :b)
+ hash = { node => node }
+ @visitor.accept hash
+ assert_equal [:a, :b, node, :a, :b, node, hash], @collector.calls
+ end
+
+ def test_update_statement
+ stmt = Nodes::UpdateStatement.new
+ stmt.relation = :a
+ stmt.values << :b
+ stmt.wheres << :c
+ stmt.orders << :d
+ stmt.limit = :e
+
+ @visitor.accept stmt
+ assert_equal [:a, :b, stmt.values, :c, stmt.wheres, :d, stmt.orders,
+ :e, stmt], @collector.calls
+ end
+
+ def test_select_core
+ core = Nodes::SelectCore.new
+ core.projections << :a
+ core.froms = :b
+ core.wheres << :c
+ core.groups << :d
+ core.windows << :e
+ core.havings << :f
+
+ @visitor.accept core
+ assert_equal [
+ :a, core.projections,
+ :b, [],
+ core.source,
+ :c, core.wheres,
+ :d, core.groups,
+ :e, core.windows,
+ :f, core.havings,
+ core], @collector.calls
+ end
+
+ def test_select_statement
+ ss = Nodes::SelectStatement.new
+ ss.cores.replace [:a]
+ ss.orders << :b
+ ss.limit = :c
+ ss.lock = :d
+ ss.offset = :e
+
+ @visitor.accept ss
+ assert_equal [
+ :a, ss.cores,
+ :b, ss.orders,
+ :c,
+ :d,
+ :e,
+ ss], @collector.calls
+ end
+
+ def test_insert_statement
+ stmt = Nodes::InsertStatement.new
+ stmt.relation = :a
+ stmt.columns << :b
+ stmt.values = :c
+
+ @visitor.accept stmt
+ assert_equal [:a, :b, stmt.columns, :c, stmt], @collector.calls
+ end
+
+ def test_case
+ node = Arel::Nodes::Case.new
+ node.case = :a
+ node.conditions << :b
+ node.default = :c
+
+ @visitor.accept node
+ assert_equal [:a, :b, node.conditions, :c, node], @collector.calls
+ end
+
+ def test_node
+ node = Nodes::Node.new
+ @visitor.accept node
+ assert_equal [node], @collector.calls
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb b/activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb
new file mode 100644
index 0000000000..a07a1a050a
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "concurrent"
+
+module Arel
+ module Visitors
+ class DummyVisitor < Visitor
+ def initialize
+ super
+ @barrier = Concurrent::CyclicBarrier.new(2)
+ end
+
+ def visit_Arel_Visitors_DummySuperNode(node)
+ 42
+ end
+
+ # This is terrible, but it's the only way to reliably reproduce
+ # the possible race where two threads attempt to correct the
+ # dispatch hash at the same time.
+ def send(*args)
+ super
+ rescue
+ # Both threads try (and fail) to dispatch to the subclass's name
+ @barrier.wait
+ raise
+ ensure
+ # Then one thread successfully completes (updating the dispatch
+ # table in the process) before the other finishes raising its
+ # exception.
+ Thread.current[:delay].wait if Thread.current[:delay]
+ end
+ end
+
+ class DummySuperNode
+ end
+
+ class DummySubNode < DummySuperNode
+ end
+
+ class DispatchContaminationTest < Arel::Spec
+ before do
+ @connection = Table.engine.connection
+ @table = Table.new(:users)
+ end
+
+ it "dispatches properly after failing upwards" do
+ node = Nodes::Union.new(Nodes::True.new, Nodes::False.new)
+ assert_equal "( TRUE UNION FALSE )", node.to_sql
+
+ node.first # from Nodes::Node's Enumerable mixin
+
+ assert_equal "( TRUE UNION FALSE )", node.to_sql
+ end
+
+ it "is threadsafe when implementing superclass fallback" do
+ visitor = DummyVisitor.new
+ main_thread_finished = Concurrent::Event.new
+
+ racing_thread = Thread.new do
+ Thread.current[:delay] = main_thread_finished
+ visitor.accept DummySubNode.new
+ end
+
+ assert_equal 42, visitor.accept(DummySubNode.new)
+ main_thread_finished.set
+
+ assert_equal 42, racing_thread.value
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/dot_test.rb b/activerecord/test/cases/arel/visitors/dot_test.rb
new file mode 100644
index 0000000000..ade53c358e
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/dot_test.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class TestDot < Arel::Test
+ def setup
+ @visitor = Visitors::Dot.new
+ end
+
+ # functions
+ [
+ Nodes::Sum,
+ Nodes::Exists,
+ Nodes::Max,
+ Nodes::Min,
+ Nodes::Avg,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ op = klass.new(:a, "z")
+ @visitor.accept op, Collectors::PlainString.new
+ end
+ end
+
+ def test_named_function
+ func = Nodes::NamedFunction.new "omg", "omg"
+ @visitor.accept func, Collectors::PlainString.new
+ end
+
+ # unary ops
+ [
+ Arel::Nodes::Not,
+ Arel::Nodes::Group,
+ Arel::Nodes::On,
+ Arel::Nodes::Grouping,
+ Arel::Nodes::Offset,
+ Arel::Nodes::Ordering,
+ Arel::Nodes::UnqualifiedColumn,
+ Arel::Nodes::ValuesList,
+ Arel::Nodes::Limit,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ op = klass.new(:a)
+ @visitor.accept op, Collectors::PlainString.new
+ end
+ end
+
+ # binary ops
+ [
+ Arel::Nodes::Assignment,
+ Arel::Nodes::Between,
+ Arel::Nodes::DoesNotMatch,
+ Arel::Nodes::Equality,
+ Arel::Nodes::GreaterThan,
+ Arel::Nodes::GreaterThanOrEqual,
+ Arel::Nodes::In,
+ Arel::Nodes::LessThan,
+ Arel::Nodes::LessThanOrEqual,
+ Arel::Nodes::Matches,
+ Arel::Nodes::NotEqual,
+ Arel::Nodes::NotIn,
+ Arel::Nodes::Or,
+ Arel::Nodes::TableAlias,
+ Arel::Nodes::As,
+ Arel::Nodes::DeleteStatement,
+ Arel::Nodes::JoinSource,
+ Arel::Nodes::Casted,
+ ].each do |klass|
+ define_method("test_#{klass.name.gsub('::', '_')}") do
+ binary = klass.new(:a, :b)
+ @visitor.accept binary, Collectors::PlainString.new
+ end
+ end
+
+ def test_Arel_Nodes_BindParam
+ node = Arel::Nodes::BindParam.new(1)
+ collector = Collectors::PlainString.new
+ assert_match '[label="<f0>Arel::Nodes::BindParam"]', @visitor.accept(node, collector).value
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/arel/visitors/ibm_db_test.rb b/activerecord/test/cases/arel/visitors/ibm_db_test.rb
new file mode 100644
index 0000000000..2ddbec3266
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/ibm_db_test.rb
@@ -0,0 +1,73 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class IbmDbTest < Arel::Spec
+ before do
+ @visitor = IBM_DB.new Table.engine.connection
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "uses FETCH FIRST n ROWS to limit results" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(1)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT FETCH FIRST 1 ROWS ONLY"
+ end
+
+ it "uses FETCH FIRST n ROWS in updates with a limit" do
+ table = Table.new(:users)
+ stmt = Nodes::UpdateStatement.new
+ stmt.relation = table
+ stmt.limit = Nodes::Limit.new(Nodes.build_quoted(1))
+ stmt.key = table[:id]
+ 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
new file mode 100644
index 0000000000..b6c2dd6ae7
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/informix_test.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class InformixTest < Arel::Spec
+ before do
+ @visitor = Informix.new Table.engine.connection
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "uses FIRST n to limit results" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(1)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT FIRST 1"
+ end
+
+ it "uses FIRST n in updates with a limit" do
+ table = Table.new(:users)
+ stmt = Nodes::UpdateStatement.new
+ stmt.relation = table
+ stmt.limit = Nodes::Limit.new(Nodes.build_quoted(1))
+ stmt.key = table[:id]
+ sql = compile(stmt)
+ sql.must_be_like "UPDATE \"users\" WHERE \"users\".\"id\" IN (SELECT FIRST 1 \"users\".\"id\" FROM \"users\")"
+ end
+
+ it "uses SKIP n to jump results" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(10)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT SKIP 10"
+ end
+
+ it "uses SKIP before FIRST" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(1)
+ stmt.offset = Nodes::Offset.new(1)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT SKIP 1 FIRST 1"
+ end
+
+ it "uses INNER JOIN to perform joins" do
+ core = Nodes::SelectCore.new
+ table = Table.new(:posts)
+ core.source = Nodes::JoinSource.new(table, [table.create_join(Table.new(:comments))])
+
+ stmt = Nodes::SelectStatement.new([core])
+ 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
new file mode 100644
index 0000000000..74f34b4dad
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/mssql_test.rb
@@ -0,0 +1,138 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class MssqlTest < Arel::Spec
+ before do
+ @visitor = MSSQL.new Table.engine.connection
+ @table = Arel::Table.new "users"
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "should not modify query if no offset or limit" do
+ stmt = Nodes::SelectStatement.new
+ sql = compile(stmt)
+ sql.must_be_like "SELECT"
+ end
+
+ it "should go over table PK if no .order() or .group()" do
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.from = @table
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY \"users\".\"id\") as _row_num FROM \"users\") as _t WHERE _row_num BETWEEN 1 AND 10"
+ end
+
+ it "caches the PK lookup for order" do
+ connection = Minitest::Mock.new
+ connection.expect(:primary_key, ["id"], ["users"])
+
+ # We don't care how many times these methods are called
+ def connection.quote_table_name(*); ""; end
+ def connection.quote_column_name(*); ""; end
+
+ @visitor = MSSQL.new(connection)
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.from = @table
+ stmt.limit = Nodes::Limit.new(10)
+
+ compile(stmt)
+ compile(stmt)
+
+ connection.verify
+ end
+
+ it "should use TOP for limited deletes" do
+ stmt = Nodes::DeleteStatement.new
+ stmt.relation = @table
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile(stmt)
+
+ sql.must_be_like "DELETE TOP (10) FROM \"users\""
+ end
+
+ it "should go over query ORDER BY if .order()" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.orders << Nodes::SqlLiteral.new("order_by")
+ sql = compile(stmt)
+ sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY order_by) as _row_num) as _t WHERE _row_num BETWEEN 1 AND 10"
+ end
+
+ it "should go over query GROUP BY if no .order() and there is .group()" do
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.groups << Nodes::SqlLiteral.new("group_by")
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY group_by) as _row_num GROUP BY group_by) as _t WHERE _row_num BETWEEN 1 AND 10"
+ end
+
+ it "should use BETWEEN if both .limit() and .offset" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.offset = Nodes::Offset.new(20)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY ) as _row_num) as _t WHERE _row_num BETWEEN 21 AND 30"
+ end
+
+ it "should use >= if only .offset" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(20)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT _t.* FROM (SELECT ROW_NUMBER() OVER (ORDER BY ) as _row_num) as _t WHERE _row_num >= 21"
+ end
+
+ it "should generate subquery for .count" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.cores.first.projections << Nodes::Count.new("*")
+ 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
new file mode 100644
index 0000000000..5f37587957
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/mysql_test.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class MysqlTest < Arel::Spec
+ before do
+ @visitor = MySQL.new Table.engine.connection
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ ###
+ # :'(
+ # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214
+ it "defaults limit to 18446744073709551615" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(1)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT FROM DUAL LIMIT 18446744073709551615 OFFSET 1"
+ end
+
+ it "should escape LIMIT" do
+ sc = Arel::Nodes::UpdateStatement.new
+ sc.relation = Table.new(:users)
+ sc.limit = Nodes::Limit.new(Nodes.build_quoted("omg"))
+ assert_equal("UPDATE \"users\" LIMIT 'omg'", compile(sc))
+ end
+
+ it "uses DUAL for empty from" do
+ stmt = Nodes::SelectStatement.new
+ sql = compile(stmt)
+ sql.must_be_like "SELECT FROM DUAL"
+ end
+
+ describe "locking" do
+ it "defaults to FOR UPDATE when locking" do
+ node = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
+ compile(node).must_be_like "FOR UPDATE"
+ end
+
+ it "allows a custom string to be used as a lock" do
+ node = Nodes::Lock.new(Arel.sql("LOCK IN SHARE MODE"))
+ compile(node).must_be_like "LOCK IN SHARE MODE"
+ end
+ end
+
+ describe "concat" do
+ it "concats columns" do
+ @table = Table.new(:users)
+ query = @table[:name].concat(@table[:name])
+ compile(query).must_be_like %{
+ CONCAT("users"."name", "users"."name")
+ }
+ end
+
+ it "concats a string" do
+ @table = Table.new(:users)
+ query = @table[:name].concat(Nodes.build_quoted("abc"))
+ compile(query).must_be_like %{
+ CONCAT("users"."name", 'abc')
+ }
+ 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
new file mode 100644
index 0000000000..ebea12910d
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/oracle12_test.rb
@@ -0,0 +1,121 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class Oracle12Test < Arel::Spec
+ before do
+ @visitor = Oracle12.new Table.engine.connection
+ @table = Table.new(:users)
+ @attr = @table[:id]
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "modified except to be minus" do
+ left = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 10")
+ right = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 20")
+ sql = compile Nodes::Except.new(left, right)
+ sql.must_be_like %{
+ ( SELECT * FROM users WHERE age > 10 MINUS SELECT * FROM users WHERE age > 20 )
+ }
+ end
+
+ it "generates select options offset then limit" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(1)
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile(stmt)
+ sql.must_be_like "SELECT OFFSET 1 ROWS FETCH FIRST 10 ROWS ONLY"
+ end
+
+ describe "locking" do
+ it "generates ArgumentError if limit and lock are used" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.lock = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
+ assert_raises ArgumentError do
+ compile(stmt)
+ end
+ end
+
+ it "defaults to FOR UPDATE when locking" do
+ node = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
+ compile(node).must_be_like "FOR UPDATE"
+ end
+ end
+
+ describe "Nodes::BindParam" do
+ it "increments each bind param" do
+ query = @table[:name].eq(Arel::Nodes::BindParam.new(1))
+ .and(@table[:id].eq(Arel::Nodes::BindParam.new(1)))
+ compile(query).must_be_like %{
+ "users"."name" = :a1 AND "users"."id" = :a2
+ }
+ 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
+
+ describe "Nodes::In" do
+ it "should know how to visit" do
+ ary = (1 .. 1001).to_a
+ node = @attr.in ary
+ compile(node).must_be_like %{
+ "users"."id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000) OR \"users\".\"id\" IN (1001)
+ }
+ end
+ end
+
+ describe "Nodes::NotIn" do
+ it "should know how to visit" do
+ ary = (1 .. 1001).to_a
+ node = @attr.not_in ary
+ compile(node).must_be_like %{
+ "users"."id" NOT IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000) AND \"users\".\"id\" NOT IN (1001)
+ }
+ 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
new file mode 100644
index 0000000000..f69b201855
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/oracle_test.rb
@@ -0,0 +1,257 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class OracleTest < Arel::Spec
+ before do
+ @visitor = Oracle.new Table.engine.connection
+ @table = Table.new(:users)
+ @attr = @table[:id]
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "modifies order when there is distinct and first value" do
+ # *sigh*
+ select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__"
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.projections << Nodes::SqlLiteral.new(select)
+ stmt.orders << Nodes::SqlLiteral.new("foo")
+ sql = compile(stmt)
+ sql.must_be_like %{
+ SELECT #{select} ORDER BY alias_0__
+ }
+ end
+
+ it "is idempotent with crazy query" do
+ # *sigh*
+ select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__"
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.projections << Nodes::SqlLiteral.new(select)
+ stmt.orders << Nodes::SqlLiteral.new("foo")
+
+ sql = compile(stmt)
+ sql2 = compile(stmt)
+ sql.must_equal sql2
+ end
+
+ it "splits orders with commas" do
+ # *sigh*
+ select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__"
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.projections << Nodes::SqlLiteral.new(select)
+ stmt.orders << Nodes::SqlLiteral.new("foo, bar")
+ sql = compile(stmt)
+ sql.must_be_like %{
+ SELECT #{select} ORDER BY alias_0__, alias_1__
+ }
+ end
+
+ it "splits orders with commas and function calls" do
+ # *sigh*
+ select = "DISTINCT foo.id, FIRST_VALUE(projects.name) OVER (foo) AS alias_0__"
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.projections << Nodes::SqlLiteral.new(select)
+ stmt.orders << Nodes::SqlLiteral.new("NVL(LOWER(bar, foo), foo) DESC, UPPER(baz)")
+ sql = compile(stmt)
+ sql.must_be_like %{
+ SELECT #{select} ORDER BY alias_0__ DESC, alias_1__
+ }
+ end
+
+ describe "Nodes::SelectStatement" do
+ describe "limit" do
+ it "adds a rownum clause" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile stmt
+ sql.must_be_like %{ SELECT WHERE ROWNUM <= 10 }
+ end
+
+ it "is idempotent" do
+ stmt = Nodes::SelectStatement.new
+ stmt.orders << Nodes::SqlLiteral.new("foo")
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile stmt
+ sql2 = compile stmt
+ sql.must_equal sql2
+ end
+
+ it "creates a subquery when there is order_by" do
+ stmt = Nodes::SelectStatement.new
+ stmt.orders << Nodes::SqlLiteral.new("foo")
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (SELECT ORDER BY foo ) WHERE ROWNUM <= 10
+ }
+ end
+
+ it "creates a subquery when there is group by" do
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.groups << Nodes::SqlLiteral.new("foo")
+ stmt.limit = Nodes::Limit.new(10)
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (SELECT GROUP BY foo ) WHERE ROWNUM <= 10
+ }
+ end
+
+ it "creates a subquery when there is DISTINCT" do
+ stmt = Nodes::SelectStatement.new
+ stmt.cores.first.set_quantifier = Arel::Nodes::Distinct.new
+ stmt.cores.first.projections << Nodes::SqlLiteral.new("id")
+ stmt.limit = Arel::Nodes::Limit.new(10)
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (SELECT DISTINCT id ) WHERE ROWNUM <= 10
+ }
+ end
+
+ it "creates a different subquery when there is an offset" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.offset = Nodes::Offset.new(10)
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM (SELECT ) raw_sql_
+ WHERE rownum <= 20
+ )
+ WHERE raw_rnum_ > 10
+ }
+ end
+
+ it "creates a subquery when there is limit and offset with BindParams" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(Nodes::BindParam.new(1))
+ stmt.offset = Nodes::Offset.new(Nodes::BindParam.new(1))
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM (SELECT ) raw_sql_
+ WHERE rownum <= (:a1 + :a2)
+ )
+ WHERE raw_rnum_ > :a3
+ }
+ end
+
+ it "is idempotent with different subquery" do
+ stmt = Nodes::SelectStatement.new
+ stmt.limit = Nodes::Limit.new(10)
+ stmt.offset = Nodes::Offset.new(10)
+ sql = compile stmt
+ sql2 = compile stmt
+ sql.must_equal sql2
+ end
+ end
+
+ describe "only offset" do
+ it "creates a select from subquery with rownum condition" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(10)
+ sql = compile stmt
+ sql.must_be_like %{
+ SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM (SELECT) raw_sql_
+ )
+ WHERE raw_rnum_ > 10
+ }
+ end
+ end
+ end
+
+ it "modified except to be minus" do
+ left = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 10")
+ right = Nodes::SqlLiteral.new("SELECT * FROM users WHERE age > 20")
+ sql = compile Nodes::Except.new(left, right)
+ sql.must_be_like %{
+ ( SELECT * FROM users WHERE age > 10 MINUS SELECT * FROM users WHERE age > 20 )
+ }
+ end
+
+ describe "locking" do
+ it "defaults to FOR UPDATE when locking" do
+ node = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
+ compile(node).must_be_like "FOR UPDATE"
+ end
+ end
+
+ describe "Nodes::BindParam" do
+ it "increments each bind param" do
+ query = @table[:name].eq(Arel::Nodes::BindParam.new(1))
+ .and(@table[:id].eq(Arel::Nodes::BindParam.new(1)))
+ compile(query).must_be_like %{
+ "users"."name" = :a1 AND "users"."id" = :a2
+ }
+ 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
+
+ describe "Nodes::In" do
+ it "should know how to visit" do
+ ary = (1 .. 1001).to_a
+ node = @attr.in ary
+ compile(node).must_be_like %{
+ "users"."id" IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000) OR \"users\".\"id\" IN (1001)
+ }
+ end
+ end
+
+ describe "Nodes::NotIn" do
+ it "should know how to visit" do
+ ary = (1 .. 1001).to_a
+ node = @attr.not_in ary
+ compile(node).must_be_like %{
+ "users"."id" NOT IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126, 127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143, 144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160, 161, 162, 163, 164, 165, 166, 167, 168, 169, 170, 171, 172, 173, 174, 175, 176, 177, 178, 179, 180, 181, 182, 183, 184, 185, 186, 187, 188, 189, 190, 191, 192, 193, 194, 195, 196, 197, 198, 199, 200, 201, 202, 203, 204, 205, 206, 207, 208, 209, 210, 211, 212, 213, 214, 215, 216, 217, 218, 219, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 239, 240, 241, 242, 243, 244, 245, 246, 247, 248, 249, 250, 251, 252, 253, 254, 255, 256, 257, 258, 259, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 270, 271, 272, 273, 274, 275, 276, 277, 278, 279, 280, 281, 282, 283, 284, 285, 286, 287, 288, 289, 290, 291, 292, 293, 294, 295, 296, 297, 298, 299, 300, 301, 302, 303, 304, 305, 306, 307, 308, 309, 310, 311, 312, 313, 314, 315, 316, 317, 318, 319, 320, 321, 322, 323, 324, 325, 326, 327, 328, 329, 330, 331, 332, 333, 334, 335, 336, 337, 338, 339, 340, 341, 342, 343, 344, 345, 346, 347, 348, 349, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 360, 361, 362, 363, 364, 365, 366, 367, 368, 369, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 383, 384, 385, 386, 387, 388, 389, 390, 391, 392, 393, 394, 395, 396, 397, 398, 399, 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 419, 420, 421, 422, 423, 424, 425, 426, 427, 428, 429, 430, 431, 432, 433, 434, 435, 436, 437, 438, 439, 440, 441, 442, 443, 444, 445, 446, 447, 448, 449, 450, 451, 452, 453, 454, 455, 456, 457, 458, 459, 460, 461, 462, 463, 464, 465, 466, 467, 468, 469, 470, 471, 472, 473, 474, 475, 476, 477, 478, 479, 480, 481, 482, 483, 484, 485, 486, 487, 488, 489, 490, 491, 492, 493, 494, 495, 496, 497, 498, 499, 500, 501, 502, 503, 504, 505, 506, 507, 508, 509, 510, 511, 512, 513, 514, 515, 516, 517, 518, 519, 520, 521, 522, 523, 524, 525, 526, 527, 528, 529, 530, 531, 532, 533, 534, 535, 536, 537, 538, 539, 540, 541, 542, 543, 544, 545, 546, 547, 548, 549, 550, 551, 552, 553, 554, 555, 556, 557, 558, 559, 560, 561, 562, 563, 564, 565, 566, 567, 568, 569, 570, 571, 572, 573, 574, 575, 576, 577, 578, 579, 580, 581, 582, 583, 584, 585, 586, 587, 588, 589, 590, 591, 592, 593, 594, 595, 596, 597, 598, 599, 600, 601, 602, 603, 604, 605, 606, 607, 608, 609, 610, 611, 612, 613, 614, 615, 616, 617, 618, 619, 620, 621, 622, 623, 624, 625, 626, 627, 628, 629, 630, 631, 632, 633, 634, 635, 636, 637, 638, 639, 640, 641, 642, 643, 644, 645, 646, 647, 648, 649, 650, 651, 652, 653, 654, 655, 656, 657, 658, 659, 660, 661, 662, 663, 664, 665, 666, 667, 668, 669, 670, 671, 672, 673, 674, 675, 676, 677, 678, 679, 680, 681, 682, 683, 684, 685, 686, 687, 688, 689, 690, 691, 692, 693, 694, 695, 696, 697, 698, 699, 700, 701, 702, 703, 704, 705, 706, 707, 708, 709, 710, 711, 712, 713, 714, 715, 716, 717, 718, 719, 720, 721, 722, 723, 724, 725, 726, 727, 728, 729, 730, 731, 732, 733, 734, 735, 736, 737, 738, 739, 740, 741, 742, 743, 744, 745, 746, 747, 748, 749, 750, 751, 752, 753, 754, 755, 756, 757, 758, 759, 760, 761, 762, 763, 764, 765, 766, 767, 768, 769, 770, 771, 772, 773, 774, 775, 776, 777, 778, 779, 780, 781, 782, 783, 784, 785, 786, 787, 788, 789, 790, 791, 792, 793, 794, 795, 796, 797, 798, 799, 800, 801, 802, 803, 804, 805, 806, 807, 808, 809, 810, 811, 812, 813, 814, 815, 816, 817, 818, 819, 820, 821, 822, 823, 824, 825, 826, 827, 828, 829, 830, 831, 832, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 843, 844, 845, 846, 847, 848, 849, 850, 851, 852, 853, 854, 855, 856, 857, 858, 859, 860, 861, 862, 863, 864, 865, 866, 867, 868, 869, 870, 871, 872, 873, 874, 875, 876, 877, 878, 879, 880, 881, 882, 883, 884, 885, 886, 887, 888, 889, 890, 891, 892, 893, 894, 895, 896, 897, 898, 899, 900, 901, 902, 903, 904, 905, 906, 907, 908, 909, 910, 911, 912, 913, 914, 915, 916, 917, 918, 919, 920, 921, 922, 923, 924, 925, 926, 927, 928, 929, 930, 931, 932, 933, 934, 935, 936, 937, 938, 939, 940, 941, 942, 943, 944, 945, 946, 947, 948, 949, 950, 951, 952, 953, 954, 955, 956, 957, 958, 959, 960, 961, 962, 963, 964, 965, 966, 967, 968, 969, 970, 971, 972, 973, 974, 975, 976, 977, 978, 979, 980, 981, 982, 983, 984, 985, 986, 987, 988, 989, 990, 991, 992, 993, 994, 995, 996, 997, 998, 999, 1000) AND \"users\".\"id\" NOT IN (1001)
+ }
+ 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
new file mode 100644
index 0000000000..0f9efb20b4
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/postgres_test.rb
@@ -0,0 +1,320 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class PostgresTest < Arel::Spec
+ before do
+ @visitor = PostgreSQL.new Table.engine.connection
+ @table = Table.new(:users)
+ @attr = @table[:id]
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ describe "locking" do
+ it "defaults to FOR UPDATE" do
+ compile(Nodes::Lock.new(Arel.sql("FOR UPDATE"))).must_be_like %{
+ FOR UPDATE
+ }
+ end
+
+ it "allows a custom string to be used as a lock" do
+ node = Nodes::Lock.new(Arel.sql("FOR SHARE"))
+ compile(node).must_be_like %{
+ FOR SHARE
+ }
+ end
+ end
+
+ it "should escape LIMIT" do
+ sc = Arel::Nodes::SelectStatement.new
+ sc.limit = Nodes::Limit.new(Nodes.build_quoted("omg"))
+ sc.cores.first.projections << Arel.sql("DISTINCT ON")
+ sc.orders << Arel.sql("xyz")
+ sql = compile(sc)
+ assert_match(/LIMIT 'omg'/, sql)
+ assert_equal 1, sql.scan(/LIMIT/).length, "should have one limit"
+ end
+
+ it "should support DISTINCT ON" do
+ core = Arel::Nodes::SelectCore.new
+ core.set_quantifier = Arel::Nodes::DistinctOn.new(Arel.sql("aaron"))
+ assert_match "DISTINCT ON ( aaron )", compile(core)
+ end
+
+ it "should support DISTINCT" do
+ core = Arel::Nodes::SelectCore.new
+ core.set_quantifier = Arel::Nodes::Distinct.new
+ assert_equal "SELECT DISTINCT", compile(core)
+ end
+
+ it "encloses LATERAL queries in parens" do
+ subquery = @table.project(:id).where(@table[:name].matches("foo%"))
+ compile(subquery.lateral).must_be_like %{
+ LATERAL (SELECT id FROM "users" WHERE "users"."name" ILIKE 'foo%')
+ }
+ end
+
+ it "produces LATERAL queries with alias" do
+ subquery = @table.project(:id).where(@table[:name].matches("foo%"))
+ compile(subquery.lateral("bar")).must_be_like %{
+ LATERAL (SELECT id FROM "users" WHERE "users"."name" ILIKE 'foo%') bar
+ }
+ end
+
+ describe "Nodes::Matches" do
+ it "should know how to visit" do
+ node = @table[:name].matches("foo%")
+ node.must_be_kind_of Nodes::Matches
+ node.case_sensitive.must_equal(false)
+ compile(node).must_be_like %{
+ "users"."name" ILIKE 'foo%'
+ }
+ end
+
+ it "should know how to visit case sensitive" do
+ node = @table[:name].matches("foo%", nil, true)
+ node.case_sensitive.must_equal(true)
+ compile(node).must_be_like %{
+ "users"."name" LIKE 'foo%'
+ }
+ end
+
+ it "can handle ESCAPE" do
+ node = @table[:name].matches("foo!%", "!")
+ compile(node).must_be_like %{
+ "users"."name" ILIKE 'foo!%' ESCAPE '!'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].matches("foo%"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" ILIKE 'foo%')
+ }
+ end
+ end
+
+ describe "Nodes::DoesNotMatch" do
+ it "should know how to visit" do
+ node = @table[:name].does_not_match("foo%")
+ node.must_be_kind_of Nodes::DoesNotMatch
+ node.case_sensitive.must_equal(false)
+ compile(node).must_be_like %{
+ "users"."name" NOT ILIKE 'foo%'
+ }
+ end
+
+ it "should know how to visit case sensitive" do
+ node = @table[:name].does_not_match("foo%", nil, true)
+ node.case_sensitive.must_equal(true)
+ compile(node).must_be_like %{
+ "users"."name" NOT LIKE 'foo%'
+ }
+ end
+
+ it "can handle ESCAPE" do
+ node = @table[:name].does_not_match("foo!%", "!")
+ compile(node).must_be_like %{
+ "users"."name" NOT ILIKE 'foo!%' ESCAPE '!'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].does_not_match("foo%"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" NOT ILIKE 'foo%')
+ }
+ end
+ end
+
+ describe "Nodes::Regexp" do
+ it "should know how to visit" do
+ node = @table[:name].matches_regexp("foo.*")
+ node.must_be_kind_of Nodes::Regexp
+ node.case_sensitive.must_equal(true)
+ compile(node).must_be_like %{
+ "users"."name" ~ 'foo.*'
+ }
+ end
+
+ it "can handle case insensitive" do
+ node = @table[:name].matches_regexp("foo.*", false)
+ node.must_be_kind_of Nodes::Regexp
+ node.case_sensitive.must_equal(false)
+ compile(node).must_be_like %{
+ "users"."name" ~* 'foo.*'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].matches_regexp("foo.*"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" ~ 'foo.*')
+ }
+ end
+ end
+
+ describe "Nodes::NotRegexp" do
+ it "should know how to visit" do
+ node = @table[:name].does_not_match_regexp("foo.*")
+ node.must_be_kind_of Nodes::NotRegexp
+ node.case_sensitive.must_equal(true)
+ compile(node).must_be_like %{
+ "users"."name" !~ 'foo.*'
+ }
+ end
+
+ it "can handle case insensitive" do
+ node = @table[:name].does_not_match_regexp("foo.*", false)
+ node.case_sensitive.must_equal(false)
+ compile(node).must_be_like %{
+ "users"."name" !~* 'foo.*'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].does_not_match_regexp("foo.*"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" !~ 'foo.*')
+ }
+ end
+ end
+
+ describe "Nodes::BindParam" do
+ it "increments each bind param" do
+ query = @table[:name].eq(Arel::Nodes::BindParam.new(1))
+ .and(@table[:id].eq(Arel::Nodes::BindParam.new(1)))
+ compile(query).must_be_like %{
+ "users"."name" = $1 AND "users"."id" = $2
+ }
+ end
+ end
+
+ describe "Nodes::Cube" do
+ it "should know how to visit with array arguments" do
+ node = Arel::Nodes::Cube.new([@table[:name], @table[:bool]])
+ compile(node).must_be_like %{
+ CUBE( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to visit with CubeDimension Argument" do
+ dimensions = Arel::Nodes::GroupingElement.new([@table[:name], @table[:bool]])
+ node = Arel::Nodes::Cube.new(dimensions)
+ compile(node).must_be_like %{
+ CUBE( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to generate parenthesis when supplied with many Dimensions" do
+ dim1 = Arel::Nodes::GroupingElement.new(@table[:name])
+ dim2 = Arel::Nodes::GroupingElement.new([@table[:bool], @table[:created_at]])
+ node = Arel::Nodes::Cube.new([dim1, dim2])
+ compile(node).must_be_like %{
+ CUBE( ( "users"."name" ), ( "users"."bool", "users"."created_at" ) )
+ }
+ end
+ end
+
+ describe "Nodes::GroupingSet" do
+ it "should know how to visit with array arguments" do
+ node = Arel::Nodes::GroupingSet.new([@table[:name], @table[:bool]])
+ compile(node).must_be_like %{
+ GROUPING SETS( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to visit with CubeDimension Argument" do
+ group = Arel::Nodes::GroupingElement.new([@table[:name], @table[:bool]])
+ node = Arel::Nodes::GroupingSet.new(group)
+ compile(node).must_be_like %{
+ GROUPING SETS( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to generate parenthesis when supplied with many Dimensions" do
+ group1 = Arel::Nodes::GroupingElement.new(@table[:name])
+ group2 = Arel::Nodes::GroupingElement.new([@table[:bool], @table[:created_at]])
+ node = Arel::Nodes::GroupingSet.new([group1, group2])
+ compile(node).must_be_like %{
+ GROUPING SETS( ( "users"."name" ), ( "users"."bool", "users"."created_at" ) )
+ }
+ end
+ end
+
+ describe "Nodes::RollUp" do
+ it "should know how to visit with array arguments" do
+ node = Arel::Nodes::RollUp.new([@table[:name], @table[:bool]])
+ compile(node).must_be_like %{
+ ROLLUP( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to visit with CubeDimension Argument" do
+ group = Arel::Nodes::GroupingElement.new([@table[:name], @table[:bool]])
+ node = Arel::Nodes::RollUp.new(group)
+ compile(node).must_be_like %{
+ ROLLUP( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to generate parenthesis when supplied with many Dimensions" do
+ group1 = Arel::Nodes::GroupingElement.new(@table[:name])
+ group2 = Arel::Nodes::GroupingElement.new([@table[:bool], @table[:created_at]])
+ node = Arel::Nodes::RollUp.new([group1, group2])
+ compile(node).must_be_like %{
+ ROLLUP( ( "users"."name" ), ( "users"."bool", "users"."created_at" ) )
+ }
+ 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
new file mode 100644
index 0000000000..ee4e07a675
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/sqlite_test.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class SqliteTest < Arel::Spec
+ before do
+ @visitor = SQLite.new Table.engine.connection
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "defaults limit to -1" do
+ stmt = Nodes::SelectStatement.new
+ stmt.offset = Nodes::Offset.new(1)
+ sql = @visitor.accept(stmt, Collectors::SQLString.new).value
+ sql.must_be_like "SELECT LIMIT -1 OFFSET 1"
+ end
+
+ it "does not support locking" do
+ node = Nodes::Lock.new(Arel.sql("FOR UPDATE"))
+ assert_equal "", @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "does not support boolean" do
+ node = Nodes::True.new()
+ assert_equal "1", @visitor.accept(node, Collectors::SQLString.new).value
+ 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
new file mode 100644
index 0000000000..625e37f1c0
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/to_sql_test.rb
@@ -0,0 +1,713 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+require "bigdecimal"
+
+module Arel
+ module Visitors
+ describe "the to_sql visitor" do
+ before do
+ @conn = FakeRecord::Base.new
+ @visitor = ToSql.new @conn.connection
+ @table = Table.new(:users)
+ @attr = @table[:id]
+ end
+
+ def compile(node)
+ @visitor.accept(node, Collectors::SQLString.new).value
+ end
+
+ it "works with BindParams" do
+ node = Nodes::BindParam.new(1)
+ sql = compile node
+ sql.must_be_like "?"
+ end
+
+ it "does not quote BindParams used as part of a ValuesList" do
+ bp = Nodes::BindParam.new(1)
+ values = Nodes::ValuesList.new([[bp]])
+ sql = compile values
+ sql.must_be_like "VALUES (?)"
+ end
+
+ it "can define a dispatch method" do
+ visited = false
+ viz = Class.new(Arel::Visitors::Visitor) {
+ define_method(:hello) do |node, c|
+ visited = true
+ end
+
+ def dispatch
+ { Arel::Table => "hello" }
+ end
+ }.new
+
+ viz.accept(@table, Collectors::SQLString.new)
+ assert visited, "hello method was called"
+ end
+
+ it "should not quote sql literals" do
+ node = @table[Arel.star]
+ sql = compile node
+ sql.must_be_like '"users".*'
+ end
+
+ it "should visit named functions" do
+ function = Nodes::NamedFunction.new("omg", [Arel.star])
+ assert_equal "omg(*)", compile(function)
+ end
+
+ it "should chain predications on named functions" do
+ function = Nodes::NamedFunction.new("omg", [Arel.star])
+ sql = compile(function.eq(2))
+ sql.must_be_like %{ omg(*) = 2 }
+ end
+
+ it "should handle nil with named functions" do
+ function = Nodes::NamedFunction.new("omg", [Arel.star])
+ sql = compile(function.eq(nil))
+ sql.must_be_like %{ omg(*) IS NULL }
+ end
+
+ it "should visit built-in functions" do
+ function = Nodes::Count.new([Arel.star])
+ assert_equal "COUNT(*)", compile(function)
+
+ function = Nodes::Sum.new([Arel.star])
+ assert_equal "SUM(*)", compile(function)
+
+ function = Nodes::Max.new([Arel.star])
+ assert_equal "MAX(*)", compile(function)
+
+ function = Nodes::Min.new([Arel.star])
+ assert_equal "MIN(*)", compile(function)
+
+ function = Nodes::Avg.new([Arel.star])
+ assert_equal "AVG(*)", compile(function)
+ end
+
+ it "should visit built-in functions operating on distinct values" do
+ function = Nodes::Count.new([Arel.star])
+ function.distinct = true
+ assert_equal "COUNT(DISTINCT *)", compile(function)
+
+ function = Nodes::Sum.new([Arel.star])
+ function.distinct = true
+ assert_equal "SUM(DISTINCT *)", compile(function)
+
+ function = Nodes::Max.new([Arel.star])
+ function.distinct = true
+ assert_equal "MAX(DISTINCT *)", compile(function)
+
+ function = Nodes::Min.new([Arel.star])
+ function.distinct = true
+ assert_equal "MIN(DISTINCT *)", compile(function)
+
+ function = Nodes::Avg.new([Arel.star])
+ function.distinct = true
+ assert_equal "AVG(DISTINCT *)", compile(function)
+ end
+
+ it "works with lists" do
+ function = Nodes::NamedFunction.new("omg", [Arel.star, Arel.star])
+ assert_equal "omg(*, *)", compile(function)
+ end
+
+ describe "Nodes::Equality" do
+ it "should escape strings" do
+ test = Table.new(:users)[:name].eq "Aaron Patterson"
+ compile(test).must_be_like %{
+ "users"."name" = 'Aaron Patterson'
+ }
+ end
+
+ it "should handle false" do
+ table = Table.new(:users)
+ val = Nodes.build_quoted(false, table[:active])
+ sql = compile Nodes::Equality.new(val, val)
+ sql.must_be_like %{ 'f' = 'f' }
+ end
+
+ it "should handle nil" do
+ sql = compile Nodes::Equality.new(@table[:name], nil)
+ sql.must_be_like %{ "users"."name" IS NULL }
+ end
+ end
+
+ describe "Nodes::Grouping" do
+ it "wraps nested groupings in brackets only once" do
+ sql = compile Nodes::Grouping.new(Nodes::Grouping.new(Nodes.build_quoted("foo")))
+ sql.must_equal "('foo')"
+ end
+ end
+
+ describe "Nodes::NotEqual" do
+ it "should handle false" do
+ val = Nodes.build_quoted(false, @table[:active])
+ sql = compile Nodes::NotEqual.new(@table[:active], val)
+ sql.must_be_like %{ "users"."active" != 'f' }
+ end
+
+ it "should handle nil" do
+ val = Nodes.build_quoted(nil, @table[:active])
+ sql = compile Nodes::NotEqual.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" IS NOT NULL }
+ 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(":'("),
+ Class.new(Class.new(String)).new(":'("),
+ ].each do |obj|
+ val = Nodes.build_quoted(obj, @table[:active])
+ sql = compile Nodes::NotEqual.new(@table[:name], val)
+ sql.must_be_like %{ "users"."name" != ':\\'(' }
+ end
+ end
+
+ it "should visit_Class" do
+ compile(Nodes.build_quoted(DateTime)).must_equal "'DateTime'"
+ end
+
+ it "should escape LIMIT" do
+ sc = Arel::Nodes::SelectStatement.new
+ sc.limit = Arel::Nodes::Limit.new(Nodes.build_quoted("omg"))
+ assert_match(/LIMIT 'omg'/, compile(sc))
+ end
+
+ it "should contain a single space before ORDER BY" do
+ table = Table.new(:users)
+ test = table.order(table[:name])
+ sql = compile test
+ assert_match(/"users" ORDER BY/, sql)
+ end
+
+ it "should quote LIMIT without column type coercion" do
+ table = Table.new(:users)
+ sc = table.where(table[:name].eq(0)).take(1).ast
+ assert_match(/WHERE "users"."name" = 0 LIMIT 1/, compile(sc))
+ end
+
+ it "should visit_DateTime" do
+ dt = DateTime.now
+ table = Table.new(:users)
+ test = table[:created_at].eq dt
+ sql = compile test
+
+ sql.must_be_like %{"users"."created_at" = '#{dt.strftime("%Y-%m-%d %H:%M:%S")}'}
+ end
+
+ it "should visit_Float" do
+ test = Table.new(:products)[:price].eq 2.14
+ sql = compile test
+ sql.must_be_like %{"products"."price" = 2.14}
+ end
+
+ it "should visit_Not" do
+ sql = compile Nodes::Not.new(Arel.sql("foo"))
+ sql.must_be_like "NOT (foo)"
+ end
+
+ it "should apply Not to the whole expression" do
+ node = Nodes::And.new [@attr.eq(10), @attr.eq(11)]
+ sql = compile Nodes::Not.new(node)
+ sql.must_be_like %{NOT ("users"."id" = 10 AND "users"."id" = 11)}
+ end
+
+ it "should visit_As" do
+ as = Nodes::As.new(Arel.sql("foo"), Arel.sql("bar"))
+ sql = compile as
+ sql.must_be_like "foo AS bar"
+ end
+
+ it "should visit_Integer" do
+ compile 8787878092
+ end
+
+ it "should visit_Hash" do
+ compile(Nodes.build_quoted(a: 1))
+ end
+
+ it "should visit_Set" do
+ compile Nodes.build_quoted(Set.new([1, 2]))
+ end
+
+ it "should visit_BigDecimal" do
+ compile Nodes.build_quoted(BigDecimal("2.14"))
+ end
+
+ it "should visit_Date" do
+ dt = Date.today
+ table = Table.new(:users)
+ test = table[:created_at].eq dt
+ sql = compile test
+
+ sql.must_be_like %{"users"."created_at" = '#{dt.strftime("%Y-%m-%d")}'}
+ end
+
+ it "should visit_NilClass" do
+ compile(Nodes.build_quoted(nil)).must_be_like "NULL"
+ end
+
+ it "unsupported input should raise UnsupportedVisitError" do
+ error = assert_raises(UnsupportedVisitError) { compile(nil) }
+ assert_match(/\AUnsupported/, error.message)
+ end
+
+ it "should visit_Arel_SelectManager, which is a subquery" do
+ mgr = Table.new(:foo).project(:bar)
+ compile(mgr).must_be_like '(SELECT bar FROM "foo")'
+ end
+
+ it "should visit_Arel_Nodes_And" do
+ node = Nodes::And.new [@attr.eq(10), @attr.eq(11)]
+ compile(node).must_be_like %{
+ "users"."id" = 10 AND "users"."id" = 11
+ }
+ end
+
+ it "should visit_Arel_Nodes_Or" do
+ node = Nodes::Or.new @attr.eq(10), @attr.eq(11)
+ compile(node).must_be_like %{
+ "users"."id" = 10 OR "users"."id" = 11
+ }
+ end
+
+ it "should visit_Arel_Nodes_Assignment" do
+ column = @table["id"]
+ node = Nodes::Assignment.new(
+ Nodes::UnqualifiedColumn.new(column),
+ Nodes::UnqualifiedColumn.new(column)
+ )
+ compile(node).must_be_like %{
+ "id" = "id"
+ }
+ end
+
+ it "should visit visit_Arel_Attributes_Time" do
+ attr = Attributes::Time.new(@attr.relation, @attr.name)
+ compile attr
+ end
+
+ it "should visit_TrueClass" do
+ test = Table.new(:users)[:bool].eq(true)
+ compile(test).must_be_like %{ "users"."bool" = 't' }
+ end
+
+ describe "Nodes::Matches" do
+ it "should know how to visit" do
+ node = @table[:name].matches("foo%")
+ compile(node).must_be_like %{
+ "users"."name" LIKE 'foo%'
+ }
+ end
+
+ it "can handle ESCAPE" do
+ node = @table[:name].matches("foo!%", "!")
+ compile(node).must_be_like %{
+ "users"."name" LIKE 'foo!%' ESCAPE '!'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].matches("foo%"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" LIKE 'foo%')
+ }
+ end
+ end
+
+ describe "Nodes::DoesNotMatch" do
+ it "should know how to visit" do
+ node = @table[:name].does_not_match("foo%")
+ compile(node).must_be_like %{
+ "users"."name" NOT LIKE 'foo%'
+ }
+ end
+
+ it "can handle ESCAPE" do
+ node = @table[:name].does_not_match("foo!%", "!")
+ compile(node).must_be_like %{
+ "users"."name" NOT LIKE 'foo!%' ESCAPE '!'
+ }
+ end
+
+ it "can handle subqueries" do
+ subquery = @table.project(:id).where(@table[:name].does_not_match("foo%"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" NOT LIKE 'foo%')
+ }
+ end
+ end
+
+ describe "Nodes::Ordering" do
+ it "should know how to visit" do
+ node = @attr.desc
+ compile(node).must_be_like %{
+ "users"."id" DESC
+ }
+ end
+ end
+
+ describe "Nodes::In" do
+ it "should know how to visit" do
+ node = @attr.in [1, 2, 3]
+ compile(node).must_be_like %{
+ "users"."id" IN (1, 2, 3)
+ }
+ end
+
+ it "should return 1=0 when empty right which is always false" do
+ node = @attr.in []
+ compile(node).must_equal "1=0"
+ end
+
+ it "can handle two dot ranges" do
+ node = @attr.between 1..3
+ compile(node).must_be_like %{
+ "users"."id" BETWEEN 1 AND 3
+ }
+ end
+
+ it "can handle three dot ranges" do
+ node = @attr.between 1...3
+ compile(node).must_be_like %{
+ "users"."id" >= 1 AND "users"."id" < 3
+ }
+ end
+
+ it "can handle ranges bounded by infinity" do
+ node = @attr.between 1..Float::INFINITY
+ compile(node).must_be_like %{
+ "users"."id" >= 1
+ }
+ node = @attr.between(-Float::INFINITY..3)
+ compile(node).must_be_like %{
+ "users"."id" <= 3
+ }
+ node = @attr.between(-Float::INFINITY...3)
+ compile(node).must_be_like %{
+ "users"."id" < 3
+ }
+ node = @attr.between(-Float::INFINITY..Float::INFINITY)
+ compile(node).must_be_like %{1=1}
+ end
+
+ it "can handle subqueries" do
+ table = Table.new(:users)
+ subquery = table.project(:id).where(table[:name].eq("Aaron"))
+ node = @attr.in subquery
+ compile(node).must_be_like %{
+ "users"."id" IN (SELECT id FROM "users" WHERE "users"."name" = 'Aaron')
+ }
+ end
+ end
+
+ describe "Nodes::InfixOperation" do
+ it "should handle Multiplication" do
+ node = Arel::Attributes::Decimal.new(Table.new(:products), :price) * Arel::Attributes::Decimal.new(Table.new(:currency_rates), :rate)
+ compile(node).must_equal %("products"."price" * "currency_rates"."rate")
+ end
+
+ it "should handle Division" do
+ node = Arel::Attributes::Decimal.new(Table.new(:products), :price) / 5
+ compile(node).must_equal %("products"."price" / 5)
+ end
+
+ it "should handle Addition" do
+ node = Arel::Attributes::Decimal.new(Table.new(:products), :price) + 6
+ compile(node).must_equal %(("products"."price" + 6))
+ end
+
+ it "should handle Subtraction" do
+ node = Arel::Attributes::Decimal.new(Table.new(:products), :price) - 7
+ compile(node).must_equal %(("products"."price" - 7))
+ end
+
+ it "should handle Concatenation" do
+ table = Table.new(:users)
+ node = table[:name].concat(table[:name])
+ compile(node).must_equal %("users"."name" || "users"."name")
+ end
+
+ it "should handle BitwiseAnd" do
+ node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) & 16
+ compile(node).must_equal %(("products"."bitmap" & 16))
+ end
+
+ it "should handle BitwiseOr" do
+ node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) | 16
+ compile(node).must_equal %(("products"."bitmap" | 16))
+ end
+
+ it "should handle BitwiseXor" do
+ node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) ^ 16
+ compile(node).must_equal %(("products"."bitmap" ^ 16))
+ end
+
+ it "should handle BitwiseShiftLeft" do
+ node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) << 4
+ compile(node).must_equal %(("products"."bitmap" << 4))
+ end
+
+ it "should handle BitwiseShiftRight" do
+ node = Arel::Attributes::Integer.new(Table.new(:products), :bitmap) >> 4
+ compile(node).must_equal %(("products"."bitmap" >> 4))
+ end
+
+ it "should handle arbitrary operators" do
+ node = Arel::Nodes::InfixOperation.new(
+ "&&",
+ Arel::Attributes::String.new(Table.new(:products), :name),
+ Arel::Attributes::String.new(Table.new(:products), :name)
+ )
+ compile(node).must_equal %("products"."name" && "products"."name")
+ end
+ end
+
+ describe "Nodes::UnaryOperation" do
+ it "should handle BitwiseNot" do
+ node = ~ Arel::Attributes::Integer.new(Table.new(:products), :bitmap)
+ compile(node).must_equal %( ~ "products"."bitmap")
+ end
+
+ it "should handle arbitrary operators" do
+ node = Arel::Nodes::UnaryOperation.new("!", Arel::Attributes::String.new(Table.new(:products), :active))
+ compile(node).must_equal %( ! "products"."active")
+ 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]
+ compile(node).must_be_like %{
+ "users"."id" NOT IN (1, 2, 3)
+ }
+ end
+
+ it "should return 1=1 when empty right which is always true" do
+ node = @attr.not_in []
+ compile(node).must_equal "1=1"
+ end
+
+ it "can handle two dot ranges" do
+ node = @attr.not_between 1..3
+ compile(node).must_equal(
+ %{("users"."id" < 1 OR "users"."id" > 3)}
+ )
+ end
+
+ it "can handle three dot ranges" do
+ node = @attr.not_between 1...3
+ compile(node).must_equal(
+ %{("users"."id" < 1 OR "users"."id" >= 3)}
+ )
+ end
+
+ it "can handle ranges bounded by infinity" do
+ node = @attr.not_between 1..Float::INFINITY
+ compile(node).must_be_like %{
+ "users"."id" < 1
+ }
+ node = @attr.not_between(-Float::INFINITY..3)
+ compile(node).must_be_like %{
+ "users"."id" > 3
+ }
+ node = @attr.not_between(-Float::INFINITY...3)
+ compile(node).must_be_like %{
+ "users"."id" >= 3
+ }
+ node = @attr.not_between(-Float::INFINITY..Float::INFINITY)
+ compile(node).must_be_like %{1=0}
+ end
+
+ it "can handle subqueries" do
+ table = Table.new(:users)
+ subquery = table.project(:id).where(table[:name].eq("Aaron"))
+ node = @attr.not_in subquery
+ compile(node).must_be_like %{
+ "users"."id" NOT IN (SELECT id FROM "users" WHERE "users"."name" = 'Aaron')
+ }
+ end
+ end
+
+ describe "Constants" do
+ it "should handle true" do
+ test = Table.new(:users).create_true
+ compile(test).must_be_like %{
+ TRUE
+ }
+ end
+
+ it "should handle false" do
+ test = Table.new(:users).create_false
+ compile(test).must_be_like %{
+ FALSE
+ }
+ end
+ end
+
+ describe "TableAlias" do
+ it "should use the underlying table for checking columns" do
+ test = Table.new(:users).alias("zomgusers")[:id].eq "3"
+ compile(test).must_be_like %{
+ "zomgusers"."id" = '3'
+ }
+ end
+ end
+
+ describe "distinct on" do
+ it "raises not implemented error" do
+ core = Arel::Nodes::SelectCore.new
+ core.set_quantifier = Arel::Nodes::DistinctOn.new(Arel.sql("aaron"))
+
+ assert_raises(NotImplementedError) do
+ compile(core)
+ end
+ end
+ end
+
+ describe "Nodes::Regexp" do
+ it "raises not implemented error" do
+ node = Arel::Nodes::Regexp.new(@table[:name], Nodes.build_quoted("foo%"))
+
+ assert_raises(NotImplementedError) do
+ compile(node)
+ end
+ end
+ end
+
+ describe "Nodes::NotRegexp" do
+ it "raises not implemented error" do
+ node = Arel::Nodes::NotRegexp.new(@table[:name], Nodes.build_quoted("foo%"))
+
+ assert_raises(NotImplementedError) do
+ compile(node)
+ end
+ end
+ end
+
+ describe "Nodes::Case" do
+ it "supports simple case expressions" do
+ node = Arel::Nodes::Case.new(@table[:name])
+ .when("foo").then(1)
+ .else(0)
+
+ compile(node).must_be_like %{
+ CASE "users"."name" WHEN 'foo' THEN 1 ELSE 0 END
+ }
+ end
+
+ it "supports extended case expressions" do
+ node = Arel::Nodes::Case.new
+ .when(@table[:name].in(%w(foo bar))).then(1)
+ .else(0)
+
+ compile(node).must_be_like %{
+ CASE WHEN "users"."name" IN ('foo', 'bar') THEN 1 ELSE 0 END
+ }
+ end
+
+ it "works without default branch" do
+ node = Arel::Nodes::Case.new(@table[:name])
+ .when("foo").then(1)
+
+ compile(node).must_be_like %{
+ CASE "users"."name" WHEN 'foo' THEN 1 END
+ }
+ end
+
+ it "allows chaining multiple conditions" do
+ node = Arel::Nodes::Case.new(@table[:name])
+ .when("foo").then(1)
+ .when("bar").then(2)
+ .else(0)
+
+ compile(node).must_be_like %{
+ CASE "users"."name" WHEN 'foo' THEN 1 WHEN 'bar' THEN 2 ELSE 0 END
+ }
+ end
+
+ it "supports #when with two arguments and no #then" do
+ node = Arel::Nodes::Case.new @table[:name]
+
+ { foo: 1, bar: 0 }.reduce(node) { |_node, pair| _node.when(*pair) }
+
+ compile(node).must_be_like %{
+ CASE "users"."name" WHEN 'foo' THEN 1 WHEN 'bar' THEN 0 END
+ }
+ end
+
+ it "can be chained as a predicate" do
+ node = @table[:name].when("foo").then("bar").else("baz")
+
+ compile(node).must_be_like %{
+ CASE "users"."name" WHEN 'foo' THEN 'bar' ELSE 'baz' END
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb
index 6b4f826766..3525fa2ab8 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -32,9 +32,27 @@ 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
+ client = Client.create!(name: "Client")
+ client.destroy!
+ assert_raise(FrozenError) { client.firm = nil }
+ assert_raise(FrozenError) { client.firm = Firm.new(name: "Firm") }
+ end
+
+ def test_eager_loading_wont_mutate_owner_record
+ client = Client.eager_load(:firm_with_basic_id).first
+ assert_not_predicate client, :firm_id_came_from_user?
+
+ client = Client.preload(:firm_with_basic_id).first
+ assert_not_predicate client, :firm_id_came_from_user?
end
def test_missing_attribute_error_is_raised_when_no_foreign_key_attribute
@@ -45,7 +63,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
ActiveRecord::SQLCounter.clear_log
Client.find(3).firm
ensure
- assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query"
+ sql_log = ActiveRecord::SQLCounter.log
+ assert sql_log.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{sql_log}"
end
def test_belongs_to_with_primary_key
@@ -265,6 +284,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal apple, citibank.firm
end
+ def test_creating_the_belonging_object_from_new_record
+ citibank = Account.new("credit_limit" => 10)
+ apple = citibank.create_firm("name" => "Apple")
+ assert_equal apple, citibank.firm
+ citibank.save
+ citibank.reload
+ assert_equal apple, citibank.firm
+ end
+
def test_creating_the_belonging_object_with_primary_key
client = Client.create(name: "Primary key client")
apple = client.create_firm_with_primary_key("name" => "Apple")
@@ -343,6 +371,30 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal "ODEGY", odegy_account.reload_firm.name
end
+ def test_reload_the_belonging_object_with_query_cache
+ odegy_account_id = accounts(:odegy_account).id
+
+ connection = ActiveRecord::Base.connection
+ connection.enable_query_cache!
+ connection.clear_query_cache
+
+ # Populate the cache with a query
+ odegy_account = Account.find(odegy_account_id)
+
+ # Populate the cache with a second query
+ odegy_account.firm
+
+ assert_equal 2, connection.query_cache.size
+
+ # Clear the cache and fetch the firm again, populating the cache with a query
+ assert_queries(1) { odegy_account.reload_firm }
+
+ # This query is not cached anymore, so it should make a real SQL query
+ assert_queries(1) { Account.find(odegy_account_id) }
+ ensure
+ ActiveRecord::Base.connection.disable_query_cache!
+ end
+
def test_natural_assignment_to_nil
client = Client.find(3)
client.firm = nil
@@ -396,8 +448,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_with_select
- assert_equal 1, Company.find(2).firm_with_select.attributes.size
- assert_equal 1, Company.all.merge!(includes: :firm_with_select).find(2).firm_with_select.attributes.size
+ assert_equal 1, Post.find(2).author_with_select.attributes.size
+ assert_equal 1, Post.includes(:author_with_select).find(2).author_with_select.attributes.size
+ end
+
+ def test_custom_attribute_with_select
+ assert_equal 2, Company.find(2).firm_with_select.attributes.size
+ assert_equal 2, Company.includes(:firm_with_select).find(2).firm_with_select.attributes.size
end
def test_belongs_to_without_counter_cache_option
@@ -428,31 +485,70 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_belongs_to_counter_with_assigning_nil
- post = Post.find(1)
- comment = Comment.find(1)
+ topic = Topic.create!(title: "debate")
+ reply = Reply.create!(title: "blah!", content: "world around!", topic: topic)
+
+ assert_equal topic.id, reply.parent_id
+ assert_equal 1, topic.reload.replies.size
+
+ reply.topic = nil
+ reply.reload
- assert_equal post.id, comment.post_id
- assert_equal 2, Post.find(post.id).comments.size
+ assert_equal topic.id, reply.parent_id
+ assert_equal 1, topic.reload.replies.size
- comment.post = nil
+ reply.topic = nil
+ reply.save!
- assert_equal 1, Post.find(post.id).comments.size
+ assert_equal 0, topic.reload.replies.size
+ end
+
+ def test_belongs_to_counter_with_assigning_new_object
+ topic = Topic.create!(title: "debate")
+ reply = Reply.create!(title: "blah!", content: "world around!", topic: topic)
+
+ assert_equal topic.id, reply.parent_id
+ assert_equal 1, topic.reload.replies_count
+
+ topic2 = reply.build_topic(title: "debate2")
+ reply.save!
+
+ assert_not_equal topic.id, reply.parent_id
+ assert_equal topic2.id, reply.parent_id
+
+ assert_equal 0, topic.reload.replies_count
+ assert_equal 1, topic2.reload.replies_count
end
def test_belongs_to_with_primary_key_counter
debate = Topic.create("title" => "debate")
debate2 = Topic.create("title" => "debate2")
- reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate")
+ reply = Reply.create("title" => "blah!", "content" => "world around!", "parent_title" => "debate2")
+
+ assert_equal 0, debate.reload.replies_count
+ assert_equal 1, debate2.reload.replies_count
+
+ reply.parent_title = "debate"
+ reply.save!
+
+ assert_equal 1, debate.reload.replies_count
+ assert_equal 0, debate2.reload.replies_count
+
+ assert_no_queries do
+ reply.topic_with_primary_key = debate
+ end
assert_equal 1, debate.reload.replies_count
assert_equal 0, debate2.reload.replies_count
reply.topic_with_primary_key = debate2
+ reply.save!
assert_equal 0, debate.reload.replies_count
assert_equal 1, debate2.reload.replies_count
reply.topic_with_primary_key = nil
+ reply.save!
assert_equal 0, debate.reload.replies_count
assert_equal 0, debate2.reload.replies_count
@@ -479,11 +575,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Topic.find(topic2.id).replies.size
reply1.topic = nil
+ reply1.save!
assert_equal 0, Topic.find(topic1.id).replies.size
assert_equal 0, Topic.find(topic2.id).replies.size
reply1.topic = topic1
+ reply1.save!
assert_equal 1, Topic.find(topic1.id).replies.size
assert_equal 0, Topic.find(topic2.id).replies.size
@@ -513,13 +611,64 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_belongs_to_counter_after_save
topic = Topic.create!(title: "monday night")
- topic.replies.create!(title: "re: monday night", content: "football")
+
+ assert_queries(2) do
+ topic.replies.create!(title: "re: monday night", content: "football")
+ end
+
assert_equal 1, Topic.find(topic.id)[:replies_count]
topic.save!
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
+ def test_belongs_to_counter_after_touch
+ topic = Topic.create!(title: "topic")
+
+ assert_equal 0, topic.replies_count
+ assert_equal 0, topic.after_touch_called
+
+ reply = Reply.create!(title: "blah!", content: "world around!", topic_with_primary_key: topic)
+
+ assert_equal 1, topic.replies_count
+ assert_equal 1, topic.after_touch_called
+
+ reply.destroy!
+
+ assert_equal 0, topic.replies_count
+ assert_equal 2, topic.after_touch_called
+ end
+
+ def test_belongs_to_touch_with_reassigning
+ debate = Topic.create!(title: "debate")
+ debate2 = Topic.create!(title: "debate2")
+ reply = Reply.create!(title: "blah!", content: "world around!", parent_title: "debate2")
+
+ time = 1.day.ago
+
+ debate.touch(time: time)
+ debate2.touch(time: time)
+
+ assert_queries(3) do
+ reply.parent_title = "debate"
+ reply.save!
+ end
+
+ assert_operator debate.reload.updated_at, :>, time
+ assert_operator debate2.reload.updated_at, :>, time
+
+ debate.touch(time: time)
+ debate2.touch(time: time)
+
+ assert_queries(3) do
+ reply.topic_with_primary_key = debate2
+ reply.save!
+ end
+
+ assert_operator debate.reload.updated_at, :>, time
+ assert_operator debate2.reload.updated_at, :>, time
+ end
+
def test_belongs_to_with_touch_option_on_touch
line_item = LineItem.create!
Invoice.create!(line_items: [line_item])
@@ -578,7 +727,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
line_item = LineItem.create!
Invoice.create!(line_items: [line_item])
- assert_queries(0) { line_item.save }
+ assert_no_queries { line_item.save }
end
def test_belongs_to_with_touch_option_on_destroy
@@ -673,7 +822,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_dont_find_target_when_foreign_key_is_null
tagging = taggings(:thinking_general)
- assert_queries(0) { tagging.super_tag }
+ assert_no_queries { tagging.super_tag }
end
def test_dont_find_target_when_saving_foreign_key_after_stale_association_loaded
@@ -693,6 +842,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
reply = Reply.create(title: "re: zoom", content: "speedy quick!")
reply.topic = topic
+ reply.save!
assert_equal 1, topic.reload[:replies_count]
assert_equal 1, topic.replies.size
@@ -748,6 +898,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
silly = SillyReply.create(title: "gaga", content: "boo-boo")
silly.reply = reply
+ silly.save!
assert_equal 1, reply.reload[:replies_count]
assert_equal 1, reply.replies.size
@@ -1034,9 +1185,20 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
comment = comments(:greetings)
- assert_difference lambda { post.reload.tags_count }, -1 do
+ assert_equal post.id, comment.id
+
+ assert_difference "post.reload.tags_count", -1 do
assert_difference "comment.reload.tags_count", +1 do
tagging.taggable = comment
+ tagging.save!
+ end
+ end
+
+ assert_difference "comment.reload.tags_count", -1 do
+ assert_difference "post.reload.tags_count", +1 do
+ tagging.taggable_type = post.class.polymorphic_name
+ tagging.taggable_id = post.id
+ tagging.save!
end
end
end
@@ -1137,17 +1299,17 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_belongs_to_with_out_of_range_value_assigning
- model = Class.new(Comment) do
+ model = Class.new(Author) do
def self.name; "Temp"; end
- validates :post, presence: true
+ validates :author_address, presence: true
end
- comment = model.new
- comment.post_id = 9223372036854775808 # out of range in the bigint
+ author = model.new
+ author.author_address_id = 9223372036854775808 # out of range in the bigint
- assert_nil comment.post
- assert_not_predicate comment, :valid?
- assert_equal [{ error: :blank }], comment.errors.details[:post]
+ assert_nil author.author_address
+ assert_not_predicate author, :valid?
+ assert_equal [{ error: :blank }], author.errors.details[:author_address]
end
def test_polymorphic_with_custom_primary_key
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index e717621928..49f754be63 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -18,7 +18,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
:categorizations, :people, :categories, :edges, :vertices
def test_eager_association_loading_with_cascaded_two_levels
- authors = Author.all.merge!(includes: { posts: :comments }, order: "authors.id").to_a
+ authors = Author.includes(posts: :comments).order(:id).to_a
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 3, authors[1].posts.size
@@ -26,7 +26,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_eager_association_loading_with_cascaded_two_levels_and_one_level
- authors = Author.all.merge!(includes: [{ posts: :comments }, :categorizations], order: "authors.id").to_a
+ authors = Author.includes({ posts: :comments }, :categorizations).order(:id).to_a
assert_equal 3, authors.size
assert_equal 5, authors[0].posts.size
assert_equal 3, authors[1].posts.size
@@ -36,7 +36,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations
- authors = Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).to_a
+ authors = Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).order(:id).to_a
assert_equal 3, assert_no_queries { authors.size }
assert_equal 10, assert_no_queries { authors[0].comments.size }
end
@@ -117,12 +117,11 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_eager_association_loading_with_has_many_sti_and_subclasses
- silly = SillyReply.new(title: "gaga", content: "boo-boo", parent_id: 1)
- silly.parent_id = 1
- assert silly.save
+ reply = Reply.new(title: "gaga", content: "boo-boo", parent_id: 1)
+ assert reply.save
topics = Topic.all.merge!(includes: :replies, order: ["topics.id", "replies_topics.id"]).to_a
- assert_no_queries do
+ assert_queries(0) do
assert_equal 2, topics[0].replies.size
assert_equal 0, topics[1].replies.size
end
@@ -161,6 +160,16 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
end
+ def test_preload_through_missing_records
+ post = Post.where.not(author_id: Author.select(:id)).preload(author: { comments: :post }).first!
+ assert_no_queries { assert_nil post.author }
+ end
+
+ def test_eager_association_loading_with_missing_first_record
+ posts = Post.where(id: 3).preload(author: { comments: :post }).to_a
+ assert_equal posts.size, 1
+ end
+
def test_eager_association_loading_with_recursive_cascading_four_levels_has_many_through
source = Vertex.all.merge!(includes: { sinks: { sinks: { sinks: :sinks } } }, order: "vertices.id").first
assert_equal vertices(:vertex_4), assert_no_queries { source.sinks.first.sinks.first.sinks.first }
diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
index 5fca972aee..673d5f1dcf 100644
--- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
+++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb
@@ -21,7 +21,7 @@ module PolymorphicFullStiClassNamesSharedTest
ActiveRecord::Base.store_full_sti_class = store_full_sti_class
post = Namespaced::Post.create(title: "Great stuff", body: "This is not", author_id: 1)
- @tagging = Tagging.create(taggable: post)
+ @tagging = post.create_tagging!
end
def teardown
diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
index c5b2b77bd4..525ad3197a 100644
--- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb
+++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb
@@ -92,7 +92,7 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase
def test_include_query
res = ShapeExpression.all.merge!(includes: [ :shape, { paint: :non_poly } ]).to_a
assert_equal NUM_SHAPE_EXPRESSIONS, res.size
- assert_queries(0) do
+ assert_no_queries do
res.each do |se|
assert_not_nil se.paint.non_poly, "this is the association that was loading incorrectly before the change"
assert_not_nil se.shape, "just making sure other associations still work"
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 6e0cf30092..594d161fa3 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -4,6 +4,7 @@ require "cases/helper"
require "models/post"
require "models/tagging"
require "models/tag"
+require "models/rating"
require "models/comment"
require "models/author"
require "models/essay"
@@ -18,6 +19,7 @@ require "models/job"
require "models/subscriber"
require "models/subscription"
require "models/book"
+require "models/citation"
require "models/developer"
require "models/computer"
require "models/project"
@@ -29,9 +31,21 @@ require "models/sponsor"
require "models/mentor"
require "models/contract"
+class EagerLoadingTooManyIdsTest < ActiveRecord::TestCase
+ fixtures :citations
+
+ def test_preloading_too_many_ids
+ assert_equal Citation.count, Citation.preload(:reference_of).to_a.size
+ end
+
+ def test_eager_loading_too_may_ids
+ assert_equal Citation.count, Citation.eager_load(:citations).offset(0).size
+ end
+end
+
class EagerAssociationTest < ActiveRecord::TestCase
fixtures :posts, :comments, :authors, :essays, :author_addresses, :categories, :categories_posts,
- :companies, :accounts, :tags, :taggings, :people, :readers, :categorizations,
+ :companies, :accounts, :tags, :taggings, :ratings, :people, :readers, :categorizations,
:owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books,
:developers, :projects, :developers_projects, :members, :memberships, :clubs, :sponsors
@@ -76,9 +90,80 @@ class EagerAssociationTest < ActiveRecord::TestCase
"expected to find only david's posts"
end
+ def test_loading_polymorphic_association_with_mixed_table_conditions
+ rating = Rating.first
+ assert_equal [taggings(:normal_comment_rating)], rating.taggings_without_tag
+
+ rating = Rating.preload(:taggings_without_tag).first
+ assert_equal [taggings(:normal_comment_rating)], rating.taggings_without_tag
+
+ rating = Rating.eager_load(:taggings_without_tag).first
+ assert_equal [taggings(:normal_comment_rating)], rating.taggings_without_tag
+ end
+
def test_loading_with_scope_including_joins
- assert_equal clubs(:boring_club), Member.preload(:general_club).find(1).general_club
- assert_equal clubs(:boring_club), Member.eager_load(:general_club).find(1).general_club
+ member = Member.first
+ assert_equal members(:groucho), member
+ assert_equal clubs(:boring_club), member.general_club
+
+ member = Member.preload(:general_club).first
+ assert_equal members(:groucho), member
+ assert_equal clubs(:boring_club), member.general_club
+
+ member = Member.eager_load(:general_club).first
+ assert_equal members(:groucho), member
+ assert_equal clubs(:boring_club), member.general_club
+ end
+
+ def test_loading_association_with_same_table_joins
+ super_memberships = [memberships(:super_membership_of_boring_club)]
+
+ member = Member.joins(:favourite_memberships).first
+ assert_equal members(:groucho), member
+ assert_equal super_memberships, member.super_memberships
+
+ member = Member.joins(:favourite_memberships).preload(:super_memberships).first
+ assert_equal members(:groucho), member
+ assert_equal super_memberships, member.super_memberships
+
+ member = Member.joins(:favourite_memberships).eager_load(:super_memberships).first
+ assert_equal members(:groucho), member
+ assert_equal super_memberships, member.super_memberships
+ end
+
+ def test_loading_association_with_intersection_joins
+ member = Member.joins(:current_membership).first
+ assert_equal members(:groucho), member
+ assert_equal clubs(:boring_club), member.club
+ assert_equal memberships(:membership_of_boring_club), member.current_membership
+
+ member = Member.joins(:current_membership).preload(:club, :current_membership).first
+ assert_equal members(:groucho), member
+ assert_equal clubs(:boring_club), member.club
+ assert_equal memberships(:membership_of_boring_club), member.current_membership
+
+ member = Member.joins(:current_membership).eager_load(:club, :current_membership).first
+ assert_equal members(:groucho), member
+ assert_equal clubs(:boring_club), member.club
+ assert_equal memberships(:membership_of_boring_club), member.current_membership
+ end
+
+ def test_loading_associations_dont_leak_instance_state
+ assertions = ->(firm) {
+ assert_equal companies(:first_firm), firm
+
+ assert_predicate firm.association(:readonly_account), :loaded?
+ assert_predicate firm.association(:accounts), :loaded?
+
+ assert_equal accounts(:signals37), firm.readonly_account
+ assert_equal [accounts(:signals37)], firm.accounts
+
+ assert_predicate firm.readonly_account, :readonly?
+ assert firm.accounts.none?(&:readonly?)
+ }
+
+ assertions.call(Firm.preload(:readonly_account, :accounts).first)
+ assertions.call(Firm.eager_load(:readonly_account, :accounts).first)
end
def test_with_ordering
@@ -1186,12 +1271,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_include_has_many_using_primary_key
expected = Firm.find(1).clients_using_primary_key.sort_by(&:name)
- # Oracle adapter truncates alias to 30 characters
- if current_adapter?(:OracleAdapter)
- firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies"[0, 30] + ".name").find(1)
- else
- firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies.name").find(1)
- end
+ firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies.name").find(1)
assert_no_queries do
assert_equal expected, firm.clients_using_primary_key
end
@@ -1273,7 +1353,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_joins_with_includes_should_preload_via_joins
post = assert_queries(1) { Post.includes(:comments).joins(:comments).order("posts.id desc").to_a.first }
- assert_queries(0) do
+ assert_no_queries do
assert_not_equal 0, post.comments.to_a.count
end
end
@@ -1320,11 +1400,24 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal expected, FirstPost.unscoped.find(2)
end
- test "preload ignores the scoping" do
- assert_equal(
- Comment.find(1).post,
- Post.where("1 = 0").scoping { Comment.preload(:post).find(1).post }
- )
+ test "belongs_to association ignores the scoping" do
+ post = Comment.find(1).post
+
+ Post.where("1=0").scoping do
+ assert_equal post, Comment.find(1).post
+ assert_equal post, Comment.preload(:post).find(1).post
+ assert_equal post, Comment.eager_load(:post).find(1).post
+ end
+ end
+
+ test "has_many association ignores the scoping" do
+ comments = Post.find(1).comments.to_a
+
+ Comment.where("1=0").scoping do
+ assert_equal comments, Post.find(1).comments
+ assert_equal comments, Post.preload(:comments).find(1).comments
+ assert_equal comments, Post.eager_load(:comments).find(1).comments
+ end
end
test "deep preload" do
@@ -1511,8 +1604,38 @@ class EagerAssociationTest < ActiveRecord::TestCase
# CollectionProxy#reader is expensive, so the preloader avoids calling it.
test "preloading has_many_through association avoids calling association.reader" do
- ActiveRecord::Associations::HasManyAssociation.any_instance.expects(:reader).never
- Author.preload(:readonly_comments).first!
+ assert_not_called_on_instance_of(ActiveRecord::Associations::HasManyAssociation, :reader) do
+ Author.preload(:readonly_comments).first!
+ end
+ end
+
+ test "preloading through a polymorphic association doesn't require the association to exist" do
+ sponsors = []
+ assert_queries 5 do
+ sponsors = Sponsor.where(sponsorable_id: 1).preload(sponsorable: [:post, :membership]).to_a
+ end
+ # check the preload worked
+ assert_queries 0 do
+ sponsors.map(&:sponsorable).map { |s| s.respond_to?(:posts) ? s.post.author : s.membership }
+ end
+ end
+
+ test "preloading a regular association through a polymorphic association doesn't require the association to exist on all types" do
+ sponsors = []
+ assert_queries 6 do
+ sponsors = Sponsor.where(sponsorable_id: 1).preload(sponsorable: [{ post: :first_comment }, :membership]).to_a
+ end
+ # check the preload worked
+ assert_queries 0 do
+ sponsors.map(&:sponsorable).map { |s| s.respond_to?(:posts) ? s.post.author : s.membership }
+ end
+ end
+
+ test "preloading a regular association with a typo through a polymorphic association still raises" do
+ # this test contains an intentional typo of first -> fist
+ assert_raises(ActiveRecord::AssociationNotFoundError) do
+ Sponsor.where(sponsorable_id: 1).preload(sponsorable: [{ post: :fist_comment }, :membership]).to_a
+ end
end
private
diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb
index 5eacb5a3d8..aef8f31112 100644
--- a/activerecord/test/cases/associations/extension_test.rb
+++ b/activerecord/test/cases/associations/extension_test.rb
@@ -89,6 +89,6 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
private
def extend!(model)
- ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) {}
+ ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) { }
end
end
diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
index 5f771fe85f..fe8bdd03ba 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]
@@ -310,7 +312,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_build
devel = Developer.find(1)
- proj = assert_no_queries(ignore_none: false) { devel.projects.build("name" => "Projekt") }
+
+ # Load schema information so we don't query below if running just this test.
+ Project.define_attribute_methods
+
+ proj = assert_no_queries { devel.projects.build("name" => "Projekt") }
assert_not_predicate devel.projects, :loaded?
assert_equal devel.projects.last, proj
@@ -325,7 +331,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_new_aliased_to_build
devel = Developer.find(1)
- proj = assert_no_queries(ignore_none: false) { devel.projects.new("name" => "Projekt") }
+
+ # Load schema information so we don't query below if running just this test.
+ Project.define_attribute_methods
+
+ proj = assert_no_queries { devel.projects.new("name" => "Projekt") }
assert_not_predicate devel.projects, :loaded?
assert_equal devel.projects.last, proj
@@ -546,7 +556,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer = project.developers.first
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_predicate project.developers, :loaded?
assert_includes project.developers, developer
end
@@ -569,7 +579,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer = Developer.create name: "Bryan", salary: 50_000
assert_not_predicate project.developers, :loaded?
- assert ! project.developers.include?(developer)
+ assert_not project.developers.include?(developer)
end
def test_find_with_merged_options
@@ -697,24 +707,13 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_join_table_alias
- # FIXME: `references` has no impact on the aliases generated for the join
- # query. The fact that we pass `:developers_projects_join` to `references`
- # and that the SQL string contains `developers_projects_join` is merely a
- # coincidence.
assert_equal(
3,
- Developer.references(:developers_projects_join).merge(
- includes: { projects: :developers },
- where: "projects_developers_projects_join.joined_on IS NOT NULL"
- ).to_a.size
+ Developer.includes(projects: :developers).where.not("projects_developers_projects_join.joined_on": nil).to_a.size
)
end
def test_join_with_group
- # FIXME: `references` has no impact on the aliases generated for the join
- # query. The fact that we pass `:developers_projects_join` to `references`
- # and that the SQL string contains `developers_projects_join` is merely a
- # coincidence.
group = Developer.columns.inject([]) do |g, c|
g << "developers.#{c.name}"
g << "developers_projects_2.#{c.name}"
@@ -723,10 +722,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal(
3,
- Developer.references(:developers_projects_join).merge(
- includes: { projects: :developers }, where: "projects_developers_projects_join.joined_on IS NOT NULL",
- group: group.join(",")
- ).to_a.size
+ Developer.includes(projects: :developers).where.not("projects_developers_projects_join.joined_on": nil).group(group.join(",")).to_a.size
)
end
@@ -755,7 +751,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_loaded_associations
developer = developers(:david)
developer.projects.reload
- assert_queries(0) do
+ assert_no_queries do
developer.project_ids
developer.project_ids
end
@@ -786,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")
@@ -795,7 +801,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_polymorphic_has_manys_works
- assert_equal [10, 20].to_set, pirates(:redbeard).treasure_estimates.map(&:price).to_set
+ assert_equal ["$10.00", "$20.00"].to_set, pirates(:redbeard).treasure_estimates.map(&:price).to_set
end
def test_symbols_as_keys
@@ -873,7 +879,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations
projects = Developer.new.projects
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_equal [], projects
assert_equal [], projects.where(title: "omg")
assert_equal [], projects.pluck(:title)
@@ -1001,16 +1007,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_has_and_belongs_to_many_while_partial_writes_false
- begin
- original_partial_writes = ActiveRecord::Base.partial_writes
- ActiveRecord::Base.partial_writes = false
- developer = Developer.new(name: "Mehmet Emin İNAÇ")
- developer.projects << Project.new(name: "Bounty")
-
- assert developer.save
- ensure
- ActiveRecord::Base.partial_writes = original_partial_writes
- end
+ original_partial_writes = ActiveRecord::Base.partial_writes
+ ActiveRecord::Base.partial_writes = false
+ developer = Developer.new(name: "Mehmet Emin İNAÇ")
+ developer.projects << Project.new(name: "Bounty")
+
+ assert developer.save
+ ensure
+ ActiveRecord::Base.partial_writes = original_partial_writes
end
def test_has_and_belongs_to_many_with_belongs_to
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index a64432fae7..6a7efe2121 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -114,7 +114,7 @@ end
class HasManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :categories, :companies, :developers, :projects,
:developers_projects, :topics, :authors, :author_addresses, :comments,
- :posts, :readers, :taggings, :cars, :jobs, :tags,
+ :posts, :readers, :taggings, :cars, :tags,
:categorizations, :zines, :interests
def setup
@@ -265,7 +265,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
@@ -295,6 +295,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
@@ -377,6 +385,27 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal invoice.id, line_item.invoice_id
end
+ class SpecialAuthor < ActiveRecord::Base
+ self.table_name = "authors"
+ has_many :books, class_name: "SpecialBook", foreign_key: :author_id
+ end
+
+ class SpecialBook < ActiveRecord::Base
+ self.table_name = "books"
+
+ belongs_to :author
+ enum read_status: { unread: 0, reading: 2, read: 3, forgotten: nil }
+ end
+
+ def test_association_enum_works_properly
+ author = SpecialAuthor.create!(name: "Test")
+ book = SpecialBook.create!(read_status: "reading")
+ author.books << book
+
+ assert_equal "reading", book.read_status
+ assert_not_equal 0, SpecialAuthor.joins(:books).where(books: { read_status: "reading" }).count
+ end
+
# When creating objects on the association, we must not do it within a scope (even though it
# would be convenient), because this would cause that scope to be applied to any callbacks etc.
def test_build_and_create_should_not_happen_within_scope
@@ -438,7 +467,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_finder_method_with_dirty_target
company = companies(:first_firm)
new_clients = []
- assert_no_queries(ignore_none: false) do
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ assert_no_queries do
new_clients << company.clients_of_firm.build(name: "Another Client")
new_clients << company.clients_of_firm.build(name: "Another Client II")
new_clients << company.clients_of_firm.build(name: "Another Client III")
@@ -458,7 +491,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_finder_bang_method_with_dirty_target
company = companies(:first_firm)
new_clients = []
- assert_no_queries(ignore_none: false) do
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ assert_no_queries do
new_clients << company.clients_of_firm.build(name: "Another Client")
new_clients << company.clients_of_firm.build(name: "Another Client II")
new_clients << company.clients_of_firm.build(name: "Another Client III")
@@ -801,6 +838,48 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_not_same original_object, collection.first, "Expected #first after #reload to return a new object"
end
+ def test_reload_with_query_cache
+ connection = ActiveRecord::Base.connection
+ connection.enable_query_cache!
+ connection.clear_query_cache
+
+ # Populate the cache with a query
+ firm = Firm.first
+ # Populate the cache with a second query
+ firm.clients.load
+
+ assert_equal 2, connection.query_cache.size
+
+ # Clear the cache and fetch the clients again, populating the cache with a query
+ assert_queries(1) { firm.clients.reload }
+ # This query is cached, so it shouldn't make a real SQL query
+ assert_queries(0) { firm.clients.load }
+
+ assert_equal 1, connection.query_cache.size
+ ensure
+ ActiveRecord::Base.connection.disable_query_cache!
+ end
+
+ def test_reloading_unloaded_associations_with_query_cache
+ connection = ActiveRecord::Base.connection
+ connection.enable_query_cache!
+ connection.clear_query_cache
+
+ firm = Firm.create!(name: "firm name")
+ client = firm.clients.create!(name: "client name")
+ firm.clients.to_a # add request to cache
+
+ connection.uncached do
+ client.update!(name: "new client name")
+ end
+
+ firm = Firm.find(firm.id)
+
+ assert_equal [client.name], firm.clients.reload.map(&:name)
+ ensure
+ ActiveRecord::Base.connection.disable_query_cache!
+ end
+
def test_find_all_with_include_and_conditions
assert_nothing_raised do
Developer.all.merge!(joins: :audit_logs, where: { "audit_logs.message" => nil, :name => "Smith" }).to_a
@@ -917,9 +996,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_predicate companies(:first_firm).clients_of_firm, :loaded?
- companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")])
+ result = companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")])
assert_equal 4, companies(:first_firm).clients_of_firm.size
assert_equal 4, companies(:first_firm).clients_of_firm.reload.size
+ assert_equal companies(:first_firm).clients_of_firm, result
end
def test_transactions_when_adding_to_persisted
@@ -935,8 +1015,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_transactions_when_adding_to_new_record
- assert_no_queries(ignore_none: false) do
- firm = Firm.new
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ firm = Firm.new
+ assert_no_queries do
firm.clients_of_firm.concat(Client.new("name" => "Natural Company"))
end
end
@@ -950,7 +1033,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_new_aliased_to_build
company = companies(:first_firm)
- new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.new("name" => "Another Client") }
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ new_client = assert_no_queries { company.clients_of_firm.new("name" => "Another Client") }
assert_not_predicate company.clients_of_firm, :loaded?
assert_equal "Another Client", new_client.name
@@ -960,7 +1047,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build
company = companies(:first_firm)
- new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") }
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
assert_not_predicate company.clients_of_firm, :loaded?
assert_equal "Another Client", new_client.name
@@ -983,6 +1074,26 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_not_empty company.contracts
end
+ def test_collection_size_with_dirty_target
+ post = posts(:thinking)
+ assert_equal [], post.reader_ids
+ assert_equal 0, post.readers.size
+ post.readers.reset
+ post.readers.build
+ assert_equal [nil], post.reader_ids
+ assert_equal 1, post.readers.size
+ end
+
+ def test_collection_empty_with_dirty_target
+ post = posts(:thinking)
+ assert_equal [], post.reader_ids
+ assert_empty post.readers
+ post.readers.reset
+ post.readers.build
+ assert_equal [nil], post.reader_ids
+ assert_not_empty post.readers
+ end
+
def test_collection_size_twice_for_regressions
post = posts(:thinking)
assert_equal 0, post.readers.size
@@ -997,7 +1108,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_many
company = companies(:first_firm)
- new_clients = assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) }
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ new_clients = assert_no_queries { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) }
assert_equal 2, new_clients.size
end
@@ -1009,10 +1124,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_without_loading_association
first_topic = topics(:first)
- Reply.column_names
assert_equal 1, first_topic.replies.length
+ # Load schema information so we don't query below if running just this test.
+ Reply.define_attribute_methods
+
assert_no_queries do
first_topic.replies.build(title: "Not saved", content: "Superstars")
assert_equal 2, first_topic.replies.size
@@ -1023,7 +1140,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_via_block
company = companies(:first_firm)
- new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build { |client| client.name = "Another Client" } }
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ new_client = assert_no_queries { company.clients_of_firm.build { |client| client.name = "Another Client" } }
assert_not_predicate company.clients_of_firm, :loaded?
assert_equal "Another Client", new_client.name
@@ -1033,7 +1154,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_many_via_block
company = companies(:first_firm)
- new_clients = assert_no_queries(ignore_none: false) do
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ new_clients = assert_no_queries do
company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) do |client|
client.name = "changed"
end
@@ -1046,8 +1171,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_create_without_loading_association
first_firm = companies(:first_firm)
- Firm.column_names
- Client.column_names
assert_equal 2, first_firm.clients_of_firm.size
first_firm.clients_of_firm.reset
@@ -1102,7 +1225,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_has_many_without_counter_cache_option
# Ship has a conventionally named `treasures_count` column, but the counter_cache
# option is not given on the association.
- ship = Ship.create(name: "Countless", treasures_count: 10)
+ ship = Ship.create!(name: "Countless", treasures_count: 10)
assert_not_predicate Ship.reflect_on_association(:treasures), :has_cached_counter?
@@ -1157,6 +1280,38 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, topic.reload.replies.size
end
+ def test_counter_cache_updates_in_memory_after_update_with_inverse_of_disabled
+ topic = Topic.create!(title: "Zoom-zoom-zoom")
+
+ assert_equal 0, topic.replies_count
+
+ reply1 = Reply.create!(title: "re: zoom", content: "speedy quick!")
+ reply2 = Reply.create!(title: "re: zoom 2", content: "OMG lol!")
+
+ assert_queries(4) do
+ topic.replies << [reply1, reply2]
+ end
+
+ assert_equal 2, topic.replies_count
+ assert_equal 2, topic.reload.replies_count
+ end
+
+ def test_counter_cache_updates_in_memory_after_update_with_inverse_of_enabled
+ category = Category.create!(name: "Counter Cache")
+
+ assert_nil category.categorizations_count
+
+ categorization1 = Categorization.create!
+ categorization2 = Categorization.create!
+
+ assert_queries(4) do
+ category.categorizations << [categorization1, categorization2]
+ end
+
+ assert_equal 2, category.categorizations_count
+ assert_equal 2, category.reload.categorizations_count
+ end
+
def test_pushing_association_updates_counter_cache
topic = Topic.order("id ASC").first
reply = Reply.create!
@@ -1194,7 +1349,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_empty_with_counter_cache
post = posts(:welcome)
- assert_queries(0) do
+ assert_no_queries do
assert_not_empty post.comments
end
end
@@ -1260,7 +1415,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
@@ -1292,8 +1447,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_transaction_when_deleting_new_record
- assert_no_queries(ignore_none: false) do
- firm = Firm.new
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ firm = Firm.new
+ assert_no_queries do
client = Client.new("name" => "New Client")
firm.clients_of_firm << client
firm.clients_of_firm.destroy(client)
@@ -1354,10 +1512,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
@@ -1554,7 +1722,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_predicate companies(:first_firm).clients_of_firm, :loaded?
clients = companies(:first_firm).clients_of_firm.to_a
- assert !clients.empty?, "37signals has clients after load"
+ assert_not clients.empty?, "37signals has clients after load"
destroyed = companies(:first_firm).clients_of_firm.destroy_all
assert_equal clients.sort_by(&:id), destroyed.sort_by(&:id)
assert destroyed.all?(&:frozen?), "destroyed clients should be frozen"
@@ -1562,6 +1730,38 @@ 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_all_on_desynced_counter_cache_association
+ category = categories(:general)
+ assert_operator category.categorizations.count, :>, 0
+
+ category.categorizations.destroy_all
+ assert_equal 0, category.categorizations.count
+ 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
@@ -1625,6 +1825,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal num_accounts, Account.count
end
+ def test_depends_and_nullify_on_polymorphic_assoc
+ author = PersonWithPolymorphicDependentNullifyComments.create!(first_name: "Laertis")
+ comment = posts(:welcome).comments.first
+ comment.author = author
+ comment.save!
+
+ assert_equal comment.author_id, author.id
+ assert_equal comment.author_type, author.class.name
+
+ author.destroy
+ comment.reload
+
+ assert_nil comment.author_id
+ assert_nil comment.author_type
+ end
+
def test_restrict_with_exception
firm = RestrictedWithExceptionFirm.create!(name: "restrict")
firm.companies.create(name: "child")
@@ -1728,7 +1944,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients = []
firm.save
- assert_queries(0, ignore_none: true) do
+ assert_no_queries do
firm.clients = []
end
@@ -1750,8 +1966,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_transactions_when_replacing_on_new_record
- assert_no_queries(ignore_none: false) do
- firm = Firm.new
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ firm = Firm.new
+ assert_no_queries do
firm.clients_of_firm = [Client.new("name" => "New Client")]
end
end
@@ -1763,7 +1982,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_loaded_associations
company = companies(:first_firm)
company.clients.reload
- assert_queries(0) do
+ assert_no_queries do
company.client_ids
company.client_ids
end
@@ -1778,7 +1997,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_counter_cache_on_unloaded_association
car = Car.create(name: "My AppliCar")
- assert_equal car.engines.size, 0
+ assert_equal 0, car.engines.size
+ end
+
+ def test_ids_reader_cache_not_used_for_size_when_association_is_dirty
+ firm = Firm.create!(name: "Startup")
+ assert_equal 0, firm.client_ids.size
+ firm.clients.build
+ assert_equal 1, firm.clients.size
+ end
+
+ def test_ids_reader_cache_should_be_cleared_when_collection_is_deleted
+ firm = companies(:first_firm)
+ assert_equal [2, 3, 11], firm.client_ids
+ client = firm.clients.first
+ firm.clients.delete(client)
+ assert_equal [3, 11], firm.client_ids
end
def test_get_ids_ignores_include_option
@@ -1790,11 +2024,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_get_ids_for_association_on_new_record_does_not_try_to_find_records
- Company.columns # Load schema information so we don't query below
- Contract.columns # if running just this test.
+ # Load schema information so we don't query below if running just this test.
+ companies(:first_client).contract_ids
company = Company.new
- assert_queries(0) do
+ assert_no_queries do
company.contract_ids
end
@@ -1839,10 +2073,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_associations_order_should_be_priority_over_throughs_order
- david = authors(:david)
+ original = authors(:david)
expected = [12, 10, 9, 8, 7, 6, 5, 3, 2, 1]
- assert_equal expected, david.comments_desc.map(&:id)
- assert_equal expected, Author.includes(:comments_desc).find(david.id).comments_desc.map(&:id)
+ assert_equal expected, original.comments_desc.map(&:id)
+ preloaded = Author.includes(:comments_desc).find(original.id)
+ assert_equal expected, preloaded.comments_desc.map(&:id)
+ assert_equal original.posts_sorted_by_id.first.comments.map(&:id), preloaded.posts_sorted_by_id.first.comments.map(&:id)
end
def test_dynamic_find_should_respect_association_order_for_through
@@ -1851,8 +2087,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
@@ -1900,7 +2136,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients.load_target
assert_predicate firm.clients, :loaded?
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
firm.clients.first
assert_equal 2, firm.clients.first(2).size
firm.clients.last
@@ -1976,8 +2212,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_many_should_defer_to_collection_if_using_a_block
firm = companies(:first_firm)
assert_queries(1) do
- firm.clients.expects(:size).never
- firm.clients.many? { true }
+ assert_not_called(firm.clients, :size) do
+ firm.clients.many? { true }
+ end
end
assert_predicate firm.clients, :loaded?
end
@@ -2009,14 +2246,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_none_on_loaded_association_should_not_use_query
firm = companies(:first_firm)
firm.clients.load # force load
- assert_no_queries { assert ! firm.clients.none? }
+ assert_no_queries { assert_not firm.clients.none? }
end
def test_calling_none_should_defer_to_collection_if_using_a_block
firm = companies(:first_firm)
assert_queries(1) do
- firm.clients.expects(:size).never
- firm.clients.none? { true }
+ assert_not_called(firm.clients, :size) do
+ firm.clients.none? { true }
+ end
end
assert_predicate firm.clients, :loaded?
end
@@ -2044,14 +2282,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_one_on_loaded_association_should_not_use_query
firm = companies(:first_firm)
firm.clients.load # force load
- assert_no_queries { assert ! firm.clients.one? }
+ assert_no_queries { assert_not firm.clients.one? }
end
def test_calling_one_should_defer_to_collection_if_using_a_block
firm = companies(:first_firm)
assert_queries(1) do
- firm.clients.expects(:size).never
- firm.clients.one? { true }
+ assert_not_called(firm.clients, :size) do
+ firm.clients.one? { true }
+ end
end
assert_predicate firm.clients, :loaded?
end
@@ -2092,9 +2331,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_association_proxy_transaction_method_starts_transaction_in_association_class
- Comment.expects(:transaction)
- Post.first.comments.transaction do
- # nothing
+ assert_called(Comment, :transaction) do
+ Post.first.comments.transaction do
+ # nothing
+ end
end
end
@@ -2110,21 +2350,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_defining_has_many_association_with_delete_all_dependency_lazily_evaluates_target_class
- ActiveRecord::Reflection::AssociationReflection.any_instance.expects(:class_name).never
- class_eval(<<-EOF, __FILE__, __LINE__ + 1)
- class DeleteAllModel < ActiveRecord::Base
- has_many :nonentities, :dependent => :delete_all
- end
- EOF
+ assert_not_called_on_instance_of(
+ ActiveRecord::Reflection::AssociationReflection,
+ :class_name,
+ ) do
+ class_eval(<<-EOF, __FILE__, __LINE__ + 1)
+ class DeleteAllModel < ActiveRecord::Base
+ has_many :nonentities, :dependent => :delete_all
+ end
+ EOF
+ end
end
def test_defining_has_many_association_with_nullify_dependency_lazily_evaluates_target_class
- ActiveRecord::Reflection::AssociationReflection.any_instance.expects(:class_name).never
- class_eval(<<-EOF, __FILE__, __LINE__ + 1)
- class NullifyModel < ActiveRecord::Base
- has_many :nonentities, :dependent => :nullify
- end
- EOF
+ assert_not_called_on_instance_of(
+ ActiveRecord::Reflection::AssociationReflection,
+ :class_name,
+ ) do
+ class_eval(<<-EOF, __FILE__, __LINE__ + 1)
+ class NullifyModel < ActiveRecord::Base
+ has_many :nonentities, :dependent => :nullify
+ end
+ EOF
+ end
end
def test_attributes_are_being_set_when_initialized_from_has_many_association_with_where_clause
@@ -2270,6 +2518,19 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_collection_association_with_private_kernel_method
firm = companies(:first_firm)
assert_equal [accounts(:signals37)], firm.accounts.open
+ assert_equal [accounts(:signals37)], firm.accounts.available
+ end
+
+ def test_association_with_or_doesnt_set_inverse_instance_key
+ firm = companies(:first_firm)
+ accounts = firm.accounts.or(Account.where(firm_id: nil)).order(:id)
+ assert_equal [firm.id, nil], accounts.map(&:firm_id)
+ end
+
+ def test_association_with_rewhere_doesnt_set_inverse_instance_key
+ firm = companies(:first_firm)
+ accounts = firm.accounts.rewhere(firm_id: [firm.id, nil]).order(:id)
+ assert_equal [firm.id, nil], accounts.map(&:firm_id)
end
test "first_or_initialize adds the record to the association" do
@@ -2301,7 +2562,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
test "has many associations on new records use null relations" do
post = Post.new
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_equal [], post.comments
assert_equal [], post.comments.where(body: "omg")
assert_equal [], post.comments.pluck(:body)
@@ -2563,6 +2824,70 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
+ test "calling size on an association that has not been loaded performs a query" do
+ car = Car.create!
+ Bulb.create(car_id: car.id)
+
+ car_two = Car.create!
+
+ assert_queries(1) do
+ assert_equal 1, car.bulbs.size
+ end
+
+ assert_queries(1) do
+ assert_equal 0, car_two.bulbs.size
+ end
+ end
+
+ test "calling size on an association that has been loaded does not perform query" do
+ car = Car.create!
+ Bulb.create(car_id: car.id)
+ car.bulb_ids
+
+ car_two = Car.create!
+ car_two.bulb_ids
+
+ assert_no_queries do
+ assert_equal 1, car.bulbs.size
+ end
+
+ assert_no_queries do
+ assert_equal 0, car_two.bulbs.size
+ end
+ end
+
+ test "calling empty on an association that has not been loaded performs a query" do
+ car = Car.create!
+ Bulb.create(car_id: car.id)
+
+ car_two = Car.create!
+
+ assert_queries(1) do
+ assert_not_empty car.bulbs
+ end
+
+ assert_queries(1) do
+ assert_empty car_two.bulbs
+ end
+ end
+
+ test "calling empty on an association that has been loaded does not performs query" do
+ car = Car.create!
+ Bulb.create(car_id: car.id)
+ car.bulb_ids
+
+ car_two = Car.create!
+ car_two.bulb_ids
+
+ assert_no_queries do
+ assert_not_empty car.bulbs
+ end
+
+ assert_no_queries do
+ assert_empty car_two.bulbs
+ end
+ end
+
class AuthorWithErrorDestroyingAssociation < ActiveRecord::Base
self.table_name = "authors"
has_many :posts_with_error_destroying,
@@ -2617,6 +2942,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_create_children_could_be_rolled_back_by_after_save
+ firm = Firm.create!(name: "A New Firm, Inc")
+ assert_no_difference "Client.count" do
+ client = firm.clients.create(name: "New Client") do |cli|
+ cli.rollback_on_save = true
+ assert_not cli.rollback_on_create_called
+ end
+ assert client.rollback_on_create_called
+ end
+ end
+
+ def test_has_many_with_out_of_range_value
+ reference = Reference.create!(id: 2147483648) # out of range in the integer
+ assert_equal [], reference.ideal_jobs
+ end
+
private
def force_signal37_to_load_all_clients_of_firm
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 3b3d4037b9..c13789f7ec 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -33,6 +33,9 @@ require "models/organization"
require "models/user"
require "models/family"
require "models/family_tree"
+require "models/section"
+require "models/seminar"
+require "models/session"
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags,
@@ -46,11 +49,24 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
Reader.create person_id: 0, post_id: 0
end
+ def test_has_many_through_create_record
+ assert books(:awdr).subscribers.create!(nick: "bob")
+ end
+
def test_marshal_dump
preloaded = Post.includes(:first_blue_tags).first
assert_equal preloaded, Marshal.load(Marshal.dump(preloaded))
end
+ def test_preload_with_nested_association
+ posts = Post.preload(:author, :author_favorites_with_scope).to_a
+
+ assert_no_queries do
+ posts.each(&:author)
+ posts.each(&:author_favorites_with_scope)
+ end
+ end
+
def test_preload_sti_rhs_class
developers = Developer.includes(:firms).all.to_a
assert_no_queries do
@@ -71,6 +87,15 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
club1.members.sort_by(&:id)
end
+ def test_preload_multiple_instances_of_the_same_record
+ club = Club.create!(name: "Aaron cool banana club")
+ Membership.create! club: club, member: Member.create!(name: "Aaron")
+ Membership.create! club: club, member: Member.create!(name: "Bob")
+
+ preloaded_clubs = Club.joins(:memberships).preload(:membership).to_a
+ assert_no_queries { preloaded_clubs.each(&:membership) }
+ end
+
def test_ordered_has_many_through
person_prime = Class.new(ActiveRecord::Base) do
def self.name; "Person"; end
@@ -191,7 +216,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
@@ -202,7 +227,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
@@ -213,17 +238,26 @@ 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)
- post.people.concat [person]
+ result = post.people.concat [person]
assert_equal 1, post.people.size
assert_equal 1, post.people.reload.size
+ assert_equal post.people, result
end
def test_associate_existing_record_twice_should_add_to_target_twice
@@ -265,7 +299,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_queries(1) { posts(:thinking) }
new_person = nil # so block binding catches it
- assert_queries(0) do
+ # Load schema information so we don't query below if running just this test.
+ Person.define_attribute_methods
+
+ assert_no_queries do
new_person = Person.new first_name: "bob"
end
@@ -285,7 +322,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_associate_new_by_building
assert_queries(1) { posts(:thinking) }
- assert_queries(0) do
+ # Load schema information so we don't query below if running just this test.
+ Person.define_attribute_methods
+
+ assert_no_queries do
posts(:thinking).people.build(first_name: "Bob")
posts(:thinking).people.new(first_name: "Ted")
end
@@ -351,7 +391,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_delete_association
- assert_queries(2) { posts(:welcome);people(:michael); }
+ assert_queries(2) { posts(:welcome); people(:michael); }
assert_queries(1) do
posts(:welcome).people.delete(people(:michael))
@@ -386,6 +426,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)) }
@@ -554,7 +618,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_replace_association
- assert_queries(4) { posts(:welcome);people(:david);people(:michael); posts(:welcome).people.reload }
+ assert_queries(4) { posts(:welcome); people(:david); people(:michael); posts(:welcome).people.reload }
# 1 query to delete the existing reader (michael)
# 1 query to associate the new reader (david)
@@ -562,15 +626,25 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
posts(:welcome).people = [people(:david)]
end
- assert_queries(0) {
+ assert_no_queries do
assert_includes posts(:welcome).people, people(:david)
assert_not_includes posts(:welcome).people, people(:michael)
- }
+ end
assert_includes posts(:welcome).reload.people.reload, people(:david)
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)]
@@ -683,13 +757,13 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_clear_associations
- assert_queries(2) { posts(:welcome);posts(:welcome).people.reload }
+ assert_queries(2) { posts(:welcome); posts(:welcome).people.reload }
assert_queries(1) do
posts(:welcome).people.clear
end
- assert_queries(0) do
+ assert_no_queries do
assert_empty posts(:welcome).people
end
@@ -737,6 +811,18 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
[:added, :before, "Roger"],
[:added, :after, "Roger"]
], log.last(4)
+
+ post.people_with_callbacks.build { |person| person.first_name = "Ted" }
+ assert_equal [
+ [:added, :before, "Ted"],
+ [:added, :after, "Ted"]
+ ], log.last(2)
+
+ post.people_with_callbacks.create { |person| person.first_name = "Sam" }
+ assert_equal [
+ [:added, :before, "Sam"],
+ [:added, :after, "Sam"]
+ ], log.last(2)
end
def test_dynamic_find_should_respect_association_include
@@ -767,7 +853,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_loaded_associations
person = people(:michael)
person.posts.reload
- assert_queries(0) do
+ assert_no_queries do
person.post_ids
person.post_ids
end
@@ -866,7 +952,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
author = authors(:mary)
category = author.named_categories.create(name: "Primary")
author.named_categories.delete(category)
- assert !Categorization.exists?(author_id: author.id, named_category_name: category.name)
+ assert_not Categorization.exists?(author_id: author.id, named_category_name: category.name)
assert_empty author.named_categories.reload
end
@@ -1177,7 +1263,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_associations_on_new_records_use_null_relations
person = Person.new
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_equal [], person.posts
assert_equal [], person.posts.where(body: "omg")
assert_equal [], person.posts.pluck(:body)
@@ -1277,6 +1363,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal authors(:david), Author.joins(:comments_for_first_author).take
end
+ def test_has_many_through_with_left_joined_same_table_with_through_table
+ assert_equal [comments(:eager_other_comment1)], authors(:mary).comments.left_joins(:post)
+ end
+
def test_has_many_through_with_unscope_should_affect_to_through_scope
assert_equal [comments(:eager_other_comment1)], authors(:mary).unordered_comments
end
@@ -1387,6 +1477,37 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [subscription2], post.subscriptions.to_a
end
+ def test_child_is_visible_to_join_model_in_add_association_callbacks
+ [:before_add, :after_add].each do |callback_name|
+ sentient_treasure = Class.new(Treasure) do
+ def self.name; "SentientTreasure"; end
+
+ has_many :pet_treasures, foreign_key: :treasure_id, callback_name => :check_pet!
+ has_many :pets, through: :pet_treasures
+
+ def check_pet!(added)
+ raise "No pet!" if added.pet.nil?
+ end
+ end
+
+ treasure = sentient_treasure.new
+ assert_nothing_raised { treasure.pets << pets(:mochi) }
+ end
+ end
+
+ def test_circular_autosave_association_correctly_saves_multiple_records
+ cs180 = Seminar.new(name: "CS180")
+ fall = Session.new(name: "Fall")
+ sections = [
+ cs180.sections.build(short_name: "A"),
+ cs180.sections.build(short_name: "B"),
+ ]
+ fall.sections << sections
+ fall.save!
+ fall.reload
+ assert_equal sections, fall.sections.sort_by(&:id)
+ end
+
private
def make_model(name)
Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index b90edf9b13..7bb629466d 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -12,6 +12,9 @@ require "models/bulb"
require "models/author"
require "models/image"
require "models/post"
+require "models/drink_designer"
+require "models/chef"
+require "models/department"
class HasOneAssociationsTest < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
@@ -22,25 +25,29 @@ 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
ActiveRecord::SQLCounter.clear_log
companies(:first_firm).account
ensure
- log_all = ActiveRecord::SQLCounter.log_all
- assert log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{log_all}"
+ sql_log = ActiveRecord::SQLCounter.log
+ assert sql_log.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{sql_log}"
end
def test_has_one_cache_nils
firm = companies(:another_firm)
assert_queries(1) { assert_nil firm.account }
- assert_queries(0) { assert_nil firm.account }
+ assert_no_queries { assert_nil firm.account }
- firms = Firm.all.merge!(includes: :account).to_a
- assert_queries(0) { firms.each(&:account) }
+ firms = Firm.includes(:account).to_a
+ assert_no_queries { firms.each(&:account) }
end
def test_with_select
@@ -110,6 +117,21 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_nil Account.find(old_account_id).firm_id
end
+ def test_nullify_on_polymorphic_association
+ department = Department.create!
+ designer = DrinkDesignerWithPolymorphicDependentNullifyChef.create!
+ chef = department.chefs.create!(employable: designer)
+
+ assert_equal chef.employable_id, designer.id
+ assert_equal chef.employable_type, designer.class.name
+
+ designer.destroy!
+ chef.reload
+
+ assert_nil chef.employable_id
+ assert_nil chef.employable_type
+ end
+
def test_nullification_on_destroyed_association
developer = Developer.create!(name: "Someone")
ship = Ship.create!(name: "Planet Caravan", developer: developer)
@@ -231,9 +253,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
end
def test_build_association_dont_create_transaction
- assert_no_queries(ignore_none: false) {
- Firm.new.build_account
- }
+ # Load schema information so we don't query below if running just this test.
+ Account.define_attribute_methods
+
+ firm = Firm.new
+ assert_no_queries do
+ firm.build_account
+ end
end
def test_building_the_associated_object_with_implicit_sti_base_class
@@ -329,6 +355,29 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal 80, odegy.reload_account.credit_limit
end
+ def test_reload_association_with_query_cache
+ odegy_id = companies(:odegy).id
+
+ connection = ActiveRecord::Base.connection
+ connection.enable_query_cache!
+ connection.clear_query_cache
+
+ # Populate the cache with a query
+ odegy = Company.find(odegy_id)
+ # Populate the cache with a second query
+ odegy.account
+
+ assert_equal 2, connection.query_cache.size
+
+ # Clear the cache and fetch the account again, populating the cache with a query
+ assert_queries(1) { odegy.reload_account }
+
+ # This query is not cached anymore, so it should make a real SQL query
+ assert_queries(1) { Company.find(odegy_id) }
+ ensure
+ ActiveRecord::Base.connection.disable_query_cache!
+ end
+
def test_build
firm = Firm.new("name" => "GlobalMegaCorp")
firm.save
@@ -452,7 +501,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal new_ship, pirate.ship
assert_predicate new_ship, :new_record?
assert_nil orig_ship.pirate_id
- assert !orig_ship.changed? # check it was saved
+ assert_not orig_ship.changed? # check it was saved
end
def test_creation_failure_with_dependent_option
@@ -661,6 +710,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
self.table_name = "books"
belongs_to :author, class_name: "SpecialAuthor"
has_one :subscription, class_name: "SpecialSupscription", foreign_key: "subscriber_id"
+
+ enum status: [:proposed, :written, :published]
end
class SpecialAuthor < ActiveRecord::Base
@@ -678,6 +729,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
book = SpecialBook.create!(status: "published")
author.book = book
+ assert_equal "published", book.status
assert_not_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count
end
diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb
index 9964f084ac..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")
@@ -64,6 +71,24 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_equal clubs(:moustache_club), new_member.club
end
+ def test_building_multiple_associations_builds_through_record
+ member_type = MemberType.create!
+ member = Member.create!
+ member_detail_with_one_association = MemberDetail.new(member_type: member_type)
+ assert_predicate member_detail_with_one_association.member, :new_record?
+ member_detail_with_two_associations = MemberDetail.new(member_type: member_type, admittable: member)
+ assert_predicate member_detail_with_two_associations.member, :new_record?
+ end
+
+ def test_creating_multiple_associations_creates_through_record
+ member_type = MemberType.create!
+ member = Member.create!
+ member_detail_with_one_association = MemberDetail.create!(member_type: member_type)
+ assert_not_predicate member_detail_with_one_association.member, :new_record?
+ member_detail_with_two_associations = MemberDetail.create!(member_type: member_type, admittable: member)
+ assert_not_predicate member_detail_with_two_associations.member, :new_record?
+ end
+
def test_creating_association_sets_both_parent_ids_for_new
member = Member.new(name: "Sean Griffin")
club = Club.new(name: "Da Club")
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index ca0620dc3b..e0dac01f4a 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -29,7 +29,10 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations_with_left_outer_joins
sql = Person.joins(agents: :agents).left_outer_joins(agents: :agents).to_sql
- assert_match(/agents_people_4/i, sql)
+ assert_match(/agents_people_2/i, sql)
+ assert_match(/INNER JOIN/i, sql)
+ assert_no_match(/agents_people_4/i, sql)
+ assert_no_match(/LEFT OUTER JOIN/i, sql)
end
def test_construct_finder_sql_does_not_table_name_collide_with_string_joins
@@ -79,19 +82,19 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
def test_find_with_implicit_inner_joins_honors_readonly_with_select
authors = Author.joins(:posts).select("authors.*").to_a
- assert !authors.empty?, "expected authors to be non-empty"
+ assert_not authors.empty?, "expected authors to be non-empty"
assert authors.all? { |a| !a.readonly? }, "expected no authors to be readonly"
end
def test_find_with_implicit_inner_joins_honors_readonly_false
authors = Author.joins(:posts).readonly(false).to_a
- assert !authors.empty?, "expected authors to be non-empty"
+ assert_not authors.empty?, "expected authors to be non-empty"
assert authors.all? { |a| !a.readonly? }, "expected no authors to be readonly"
end
def test_find_with_implicit_inner_joins_does_not_set_associations
authors = Author.joins(:posts).select("authors.*").to_a
- assert !authors.empty?, "expected authors to be non-empty"
+ assert_not authors.empty?, "expected authors to be non-empty"
assert authors.all? { |a| !a.instance_variable_defined?(:@posts) }, "expected no authors to have the @posts association loaded"
end
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index 896bf574f4..da3a42e2b5 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -119,11 +119,11 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
def test_polymorphic_and_has_many_through_relationships_should_not_have_inverses
sponsor_reflection = Sponsor.reflect_on_association(:sponsorable)
- assert !sponsor_reflection.has_inverse?, "A polymorphic association should not find an inverse automatically"
+ assert_not sponsor_reflection.has_inverse?, "A polymorphic association should not find an inverse automatically"
club_reflection = Club.reflect_on_association(:members)
- assert !club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically"
+ assert_not club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically"
end
def test_polymorphic_has_one_should_find_inverse_automatically
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 57ebc1e3e0..9d1c73c33b 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -732,7 +732,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
category = Category.create!(name: "Not Associated")
assert_not_predicate david.categories, :loaded?
- assert ! david.categories.include?(category)
+ assert_not david.categories.include?(category)
end
def test_has_many_through_goes_through_all_sti_classes
diff --git a/activerecord/test/cases/associations/left_outer_join_association_test.rb b/activerecord/test/cases/associations/left_outer_join_association_test.rb
index 0e54e8c1b0..0a8863c35d 100644
--- a/activerecord/test/cases/associations/left_outer_join_association_test.rb
+++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb
@@ -46,6 +46,12 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase
assert queries.any? { |sql| /LEFT OUTER JOIN/i.match?(sql) }
end
+ def test_left_outer_joins_is_deduped_when_same_association_is_joined
+ queries = capture_sql { Author.joins(:posts).left_outer_joins(:posts).to_a }
+ assert queries.any? { |sql| /INNER JOIN/i.match?(sql) }
+ assert queries.none? { |sql| /LEFT OUTER JOIN/i.match?(sql) }
+ end
+
def test_construct_finder_sql_ignores_empty_left_outer_joins_hash
queries = capture_sql { Author.left_outer_joins({}).to_a }
assert queries.none? { |sql| /LEFT OUTER JOIN/i.match?(sql) }
@@ -60,6 +66,10 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase
assert_raise(ArgumentError) { Author.left_outer_joins('LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"').to_a }
end
+ def test_left_outer_joins_with_string_join
+ assert_equal 16, Author.left_outer_joins(:posts).joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id").count
+ end
+
def test_join_conditions_added_to_join_clause
queries = capture_sql { Author.left_outer_joins(:essays).to_a }
assert queries.any? { |sql| /writer_type.*?=.*?(Author|\?|\$1|\:a1)/i.match?(sql) }
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index 03ed1c1d47..35da74102d 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -137,7 +137,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
def test_has_many_through_has_one_through_with_has_one_source_reflection_preload
members = assert_queries(4) { Member.includes(:nested_sponsors).to_a }
mustache = sponsors(:moustache_club_sponsor_for_groucho)
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_equal [mustache], members.first.nested_sponsors
end
end
@@ -196,7 +196,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
# postgresql test if randomly executed then executes "SHOW max_identifier_length". Hence
# the need to ignore certain predefined sqls that deal with system calls.
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_equal [groucho_details, other_details], members.first.organization_member_details_2.sort_by(&:id)
end
end
@@ -548,6 +548,15 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_through_association_preload_doesnt_reset_source_association_if_already_preloaded
+ blue = tags(:blue)
+ authors = Author.preload(posts: :first_blue_tags_2, misc_post_first_blue_tags_2: {}).to_a.sort_by(&:id)
+
+ assert_no_queries do
+ assert_equal [blue], authors[2].posts.first.first_blue_tags_2
+ end
+ end
+
def test_nested_has_many_through_with_conditions_on_source_associations_preload_via_joins
# Pointless condition to force single-query loading
assert_includes_and_joins_equal(
@@ -610,6 +619,12 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
assert_equal hotel, Hotel.joins(:cake_designers, :drink_designers).take
end
+ def test_has_many_through_reset_source_reflection_after_loading_is_complete
+ preloaded = Category.preload(:ordered_post_comments).find(1, 2).last
+ original = Category.find(2)
+ assert_equal original.ordered_post_comments.ids, preloaded.ordered_post_comments.ids
+ end
+
private
def assert_includes_and_joins_equal(query, expected, association)
diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb
index 65a3bb5efe..c7a78e6bc4 100644
--- a/activerecord/test/cases/associations/required_test.rb
+++ b/activerecord/test/cases/associations/required_test.rb
@@ -25,20 +25,18 @@ class RequiredAssociationsTest < ActiveRecord::TestCase
end
test "belongs_to associations can be optional by default" do
- begin
- original_value = ActiveRecord::Base.belongs_to_required_by_default
- ActiveRecord::Base.belongs_to_required_by_default = false
+ original_value = ActiveRecord::Base.belongs_to_required_by_default
+ ActiveRecord::Base.belongs_to_required_by_default = false
- model = subclass_of(Child) do
- belongs_to :parent, inverse_of: false,
- class_name: "RequiredAssociationsTest::Parent"
- end
-
- assert model.new.save
- assert model.new(parent: Parent.new).save
- ensure
- ActiveRecord::Base.belongs_to_required_by_default = original_value
+ model = subclass_of(Child) do
+ belongs_to :parent, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
end
+
+ assert model.new.save
+ assert model.new(parent: Parent.new).save
+ ensure
+ ActiveRecord::Base.belongs_to_required_by_default = original_value
end
test "required belongs_to associations have presence validated" do
@@ -56,24 +54,22 @@ class RequiredAssociationsTest < ActiveRecord::TestCase
end
test "belongs_to associations can be required by default" do
- begin
- original_value = ActiveRecord::Base.belongs_to_required_by_default
- ActiveRecord::Base.belongs_to_required_by_default = true
+ original_value = ActiveRecord::Base.belongs_to_required_by_default
+ ActiveRecord::Base.belongs_to_required_by_default = true
- model = subclass_of(Child) do
- belongs_to :parent, inverse_of: false,
- class_name: "RequiredAssociationsTest::Parent"
- end
+ model = subclass_of(Child) do
+ belongs_to :parent, inverse_of: false,
+ class_name: "RequiredAssociationsTest::Parent"
+ end
- record = model.new
- assert_not record.save
- assert_equal ["Parent must exist"], record.errors.full_messages
+ record = model.new
+ assert_not record.save
+ assert_equal ["Parent must exist"], record.errors.full_messages
- record.parent = Parent.new
- assert record.save
- ensure
- ActiveRecord::Base.belongs_to_required_by_default = original_value
- end
+ record.parent = Parent.new
+ assert record.save
+ ensure
+ ActiveRecord::Base.belongs_to_required_by_default = original_value
end
test "has_one associations are not required by default" do
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index fce2a7eed1..84130ec208 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -21,6 +21,11 @@ require "models/molecule"
require "models/electron"
require "models/man"
require "models/interest"
+require "models/pirate"
+require "models/parrot"
+require "models/bird"
+require "models/treasure"
+require "models/price_estimate"
class AssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :developers_projects,
@@ -80,7 +85,7 @@ class AssociationsTest < ActiveRecord::TestCase
def test_force_reload
firm = Firm.new("name" => "A New Firm, Inc")
firm.save
- firm.clients.each {} # forcing to load all clients
+ firm.clients.each { } # forcing to load all clients
assert firm.clients.empty?, "New firm shouldn't have client objects"
assert_equal 0, firm.clients.size, "New firm should have 0 clients"
@@ -92,7 +97,7 @@ class AssociationsTest < ActiveRecord::TestCase
firm.clients.reload
- assert !firm.clients.empty?, "New firm should have reloaded client objects"
+ assert_not firm.clients.empty?, "New firm should have reloaded client objects"
assert_equal 1, firm.clients.size, "New firm should have reloaded clients count"
end
@@ -102,8 +107,8 @@ class AssociationsTest < ActiveRecord::TestCase
has_many_reflections = [Tag.reflect_on_association(:taggings), Developer.reflect_on_association(:projects)]
mixed_reflections = (belongs_to_reflections + has_many_reflections).uniq
assert using_limitable_reflections.call(belongs_to_reflections), "Belong to associations are limitable"
- assert !using_limitable_reflections.call(has_many_reflections), "All has many style associations are not limitable"
- assert !using_limitable_reflections.call(mixed_reflections), "No collection associations (has many style) should pass"
+ assert_not using_limitable_reflections.call(has_many_reflections), "All has many style associations are not limitable"
+ assert_not using_limitable_reflections.call(mixed_reflections), "No collection associations (has many style) should pass"
end
def test_association_with_references
@@ -368,3 +373,97 @@ class GeneratedMethodsTest < ActiveRecord::TestCase
assert_equal :none, MyArticle.new.comments
end
end
+
+class WithAnnotationsTest < ActiveRecord::TestCase
+ fixtures :pirates, :parrots
+
+ def test_belongs_to_with_annotation_includes_a_query_comment
+ pirate = SpacePirate.where.not(parrot_id: nil).first
+ assert pirate, "should have a Pirate record"
+
+ log = capture_sql do
+ pirate.parrot
+ end
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
+
+ assert_sql(%r{/\* that tells jokes \*/}) do
+ pirate.parrot_with_annotation
+ end
+ end
+
+ def test_has_and_belongs_to_many_with_annotation_includes_a_query_comment
+ pirate = SpacePirate.first
+ assert pirate, "should have a Pirate record"
+
+ log = capture_sql do
+ pirate.parrots.first
+ end
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
+
+ assert_sql(%r{/\* that are very colorful \*/}) do
+ pirate.parrots_with_annotation.first
+ end
+ end
+
+ def test_has_one_with_annotation_includes_a_query_comment
+ pirate = SpacePirate.first
+ assert pirate, "should have a Pirate record"
+
+ log = capture_sql do
+ pirate.ship
+ end
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
+
+ assert_sql(%r{/\* that is a rocket \*/}) do
+ pirate.ship_with_annotation
+ end
+ end
+
+ def test_has_many_with_annotation_includes_a_query_comment
+ pirate = SpacePirate.first
+ assert pirate, "should have a Pirate record"
+
+ log = capture_sql do
+ pirate.birds.first
+ end
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
+
+ assert_sql(%r{/\* that are also parrots \*/}) do
+ pirate.birds_with_annotation.first
+ end
+ end
+
+ def test_has_many_through_with_annotation_includes_a_query_comment
+ pirate = SpacePirate.first
+ assert pirate, "should have a Pirate record"
+
+ log = capture_sql do
+ pirate.treasure_estimates.first
+ end
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
+
+ assert_sql(%r{/\* yarrr \*/}) do
+ pirate.treasure_estimates_with_annotation.first
+ end
+ end
+
+ def test_has_many_through_with_annotation_includes_a_query_comment_when_eager_loading
+ pirate = SpacePirate.first
+ assert pirate, "should have a Pirate record"
+
+ log = capture_sql do
+ pirate.treasure_estimates.first
+ end
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
+
+ assert_sql(%r{/\* yarrr \*/}) do
+ SpacePirate.includes(:treasure_estimates_with_annotation, :treasures).first
+ end
+ end
+end
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index dc6638d45d..9fd62dcf72 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!"
@@ -63,8 +70,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase
t.author_name = ""
assert t.attribute_present?("title")
assert t.attribute_present?("written_on")
- assert !t.attribute_present?("content")
- assert !t.attribute_present?("author_name")
+ assert_not t.attribute_present?("content")
+ assert_not t.attribute_present?("author_name")
end
test "attribute_present with booleans" do
@@ -77,7 +84,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert b2.attribute_present?(:value)
b3 = Boolean.new
- assert !b3.attribute_present?(:value)
+ assert_not b3.attribute_present?(:value)
b4 = Boolean.new
b4.value = false
@@ -163,19 +170,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "10", keyboard.read_attribute_before_type_cast(:key_number)
end
- # Syck calls respond_to? before actually calling initialize.
- test "respond_to? with an allocated object" do
- klass = Class.new(ActiveRecord::Base) do
- self.table_name = "topics"
- end
-
- topic = klass.allocate
- assert_not_respond_to topic, "nothingness"
- assert_not_respond_to topic, :nothingness
- assert_respond_to topic, "title"
- assert_respond_to topic, :title
- end
-
# IRB inspects the return value of MyModel.allocate.
test "allocated objects can be inspected" do
topic = Topic.allocate
@@ -323,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"
@@ -354,9 +360,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase
test "read_attribute when false" do
topic = topics(:first)
topic.approved = false
- assert !topic.approved?, "approved should be false"
+ assert_not topic.approved?, "approved should be false"
topic.approved = "false"
- assert !topic.approved?, "approved should be false"
+ assert_not topic.approved?, "approved should be false"
end
test "read_attribute when true" do
@@ -370,10 +376,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
test "boolean attributes writing and reading" do
topic = Topic.new
topic.approved = "false"
- assert !topic.approved?, "approved should be false"
+ assert_not topic.approved?, "approved should be false"
topic.approved = "false"
- assert !topic.approved?, "approved should be false"
+ assert_not topic.approved?, "approved should be false"
topic.approved = "true"
assert topic.approved?, "approved should be true"
@@ -427,6 +433,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
assert_equal true, Topic.new(author_name: "Name").author_name?
+
+ ActiveModel::Type::Boolean::FALSE_VALUES.each do |value|
+ assert_predicate Topic.new(author_name: value), :author_name?
+ end
end
test "number attribute predicate" do
@@ -448,8 +458,71 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
+ test "user-defined text attribute predicate" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = Topic.table_name
+
+ attribute :user_defined_text, :text
+ end
+
+ topic = klass.new(user_defined_text: "text")
+ assert_predicate topic, :user_defined_text?
+
+ ActiveModel::Type::Boolean::FALSE_VALUES.each do |value|
+ topic = klass.new(user_defined_text: value)
+ assert_predicate topic, :user_defined_text?
+ end
+ end
+
+ test "user-defined date attribute predicate" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = Topic.table_name
+
+ attribute :user_defined_date, :date
+ end
+
+ topic = klass.new(user_defined_date: Date.current)
+ assert_predicate topic, :user_defined_date?
+ end
+
+ test "user-defined datetime attribute predicate" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = Topic.table_name
+
+ attribute :user_defined_datetime, :datetime
+ end
+
+ topic = klass.new(user_defined_datetime: Time.current)
+ assert_predicate topic, :user_defined_datetime?
+ end
+
+ test "user-defined time attribute predicate" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = Topic.table_name
+
+ attribute :user_defined_time, :time
+ end
+
+ topic = klass.new(user_defined_time: Time.current)
+ assert_predicate topic, :user_defined_time?
+ end
+
+ test "user-defined json attribute predicate" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = Topic.table_name
+
+ attribute :user_defined_json, :json
+ end
+
+ topic = klass.new(user_defined_json: { key: "value" })
+ assert_predicate topic, :user_defined_json?
+
+ topic = klass.new(user_defined_json: {})
+ assert_not_predicate topic, :user_defined_json?
+ 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
@@ -705,6 +778,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
record.written_on = "Jan 01 00:00:00 2014"
assert_equal record, YAML.load(YAML.dump(record))
end
+ ensure
+ # NOTE: Reset column info because global topics
+ # don't have tz-aware attributes by default.
+ Topic.reset_column_information
end
test "setting a time zone-aware time in the current time zone" do
@@ -736,6 +813,16 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
end
+ test "setting invalid string to a zone-aware time attribute" do
+ in_time_zone "Pacific Time (US & Canada)" do
+ record = @target.new
+ time_string = "ABC"
+
+ record.bonus_time = time_string
+ assert_nil record.bonus_time
+ end
+ end
+
test "removing time zone-aware types" do
with_time_zone_aware_types(:datetime) do
in_time_zone "Pacific Time (US & Canada)" do
@@ -827,7 +914,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
self.table_name = "computers"
end
- assert !klass.instance_method_already_implemented?(:system)
+ assert_not klass.instance_method_already_implemented?(:system)
computer = klass.new
assert_nil computer.system
end
@@ -841,8 +928,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase
self.table_name = "computers"
end
- assert !klass.instance_method_already_implemented?(:system)
- assert !subklass.instance_method_already_implemented?(:system)
+ assert_not klass.instance_method_already_implemented?(:system)
+ assert_not subklass.instance_method_already_implemented?(:system)
computer = subklass.new
assert_nil computer.system
end
@@ -996,7 +1083,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
test "generated attribute methods ancestors have correct class" do
mod = Topic.send(:generated_attribute_methods)
- assert_match %r(GeneratedAttributeMethods), mod.inspect
+ assert_match %r(Topic::GeneratedAttributeMethods), mod.inspect
end
private
diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb
index 3bc56694be..a6fb9f0af7 100644
--- a/activerecord/test/cases/attributes_test.rb
+++ b/activerecord/test/cases/attributes_test.rb
@@ -56,6 +56,17 @@ module ActiveRecord
assert_equal 255, UnoverloadedType.type_for_attribute("overloaded_string_with_limit").limit
end
+ test "extra options are forwarded to the type caster constructor" do
+ klass = Class.new(OverloadedType) do
+ attribute :starts_at, :datetime, precision: 3, limit: 2, scale: 1
+ end
+
+ starts_at_type = klass.type_for_attribute(:starts_at)
+ assert_equal 3, starts_at_type.precision
+ assert_equal 2, starts_at_type.limit
+ assert_equal 1, starts_at_type.scale
+ end
+
test "nonexistent attribute" do
data = OverloadedType.new(non_existent_decimal: 1)
@@ -148,6 +159,20 @@ module ActiveRecord
assert_equal 2, klass.new.counter
end
+ test "procs for default values are evaluated even after column_defaults is called" do
+ klass = Class.new(OverloadedType) do
+ @@counter = 0
+ attribute :counter, :integer, default: -> { @@counter += 1 }
+ end
+
+ assert_equal 1, klass.new.counter
+
+ # column_defaults will increment the counter since the proc is called
+ klass.column_defaults
+
+ assert_equal 3, klass.new.counter
+ end
+
test "procs are memoized before type casting" do
klass = Class.new(OverloadedType) do
@@counter = 0
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index b8243d148a..1a0732c14b 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "cases/helper"
+require "models/author"
require "models/bird"
require "models/post"
require "models/comment"
@@ -14,6 +15,7 @@ require "models/line_item"
require "models/order"
require "models/parrot"
require "models/pirate"
+require "models/project"
require "models/ship"
require "models/ship_part"
require "models/tag"
@@ -27,6 +29,7 @@ require "models/member_detail"
require "models/organization"
require "models/guitar"
require "models/tuning_peg"
+require "models/reply"
class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
def test_autosave_validation
@@ -116,7 +119,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
assert_not_predicate firm.account, :valid?
assert_not_predicate firm, :valid?
- assert !firm.save
+ assert_not firm.save
assert_equal ["is invalid"], firm.errors["account"]
end
@@ -237,7 +240,7 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
log.developer = Developer.new
assert_not_predicate log.developer, :valid?
assert_not_predicate log, :valid?
- assert !log.save
+ assert_not log.save
assert_equal ["is invalid"], log.errors["developer"]
end
@@ -499,10 +502,10 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
def test_invalid_adding
firm = Firm.find(1)
- assert !(firm.clients_of_firm << c = Client.new)
+ assert_not (firm.clients_of_firm << c = Client.new)
assert_not_predicate c, :persisted?
assert_not_predicate firm, :valid?
- assert !firm.save
+ assert_not firm.save
assert_not_predicate c, :persisted?
end
@@ -512,11 +515,23 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
assert_not_predicate c, :persisted?
assert_not_predicate c, :valid?
assert_not_predicate new_firm, :valid?
- assert !new_firm.save
+ assert_not new_firm.save
assert_not_predicate c, :persisted?
assert_not_predicate new_firm, :persisted?
end
+ def test_adding_unsavable_association
+ new_firm = Firm.new("name" => "A New Firm, Inc")
+ client = new_firm.clients.new("name" => "Apple")
+ client.throw_on_save = true
+
+ assert_predicate client, :valid?
+ assert_predicate new_firm, :valid?
+ assert_not new_firm.save
+ assert_not_predicate new_firm, :persisted?
+ assert_not_predicate client, :persisted?
+ end
+
def test_invalid_adding_with_validate_false
firm = Firm.first
client = Client.new
@@ -545,12 +560,40 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
assert_equal no_of_clients + 1, Client.count
end
+ def test_parent_should_save_children_record_with_foreign_key_validation_set_in_before_save_callback
+ company = NewlyContractedCompany.new(name: "test")
+
+ assert company.save
+ assert_not_empty company.reload.new_contracts
+ end
+
+ def test_parent_should_not_get_saved_with_duplicate_children_records
+ assert_no_difference "Reply.count" do
+ assert_no_difference "SillyUniqueReply.count" do
+ reply = Reply.new
+ reply.silly_unique_replies.build([
+ { content: "Best content" },
+ { content: "Best content" }
+ ])
+
+ assert_not reply.save
+ assert_equal ["is invalid"], reply.errors[:silly_unique_replies]
+ assert_empty reply.silly_unique_replies.first.errors
+
+ assert_equal(
+ ["has already been taken"],
+ reply.silly_unique_replies.last.errors[:content]
+ )
+ end
+ end
+ end
+
def test_invalid_build
new_client = companies(:first_firm).clients_of_firm.build
assert_not_predicate new_client, :persisted?
assert_not_predicate new_client, :valid?
assert_equal new_client, companies(:first_firm).clients_of_firm.last
- assert !companies(:first_firm).save
+ assert_not companies(:first_firm).save
assert_not_predicate new_client, :persisted?
assert_equal 2, companies(:first_firm).clients_of_firm.reload.size
end
@@ -600,7 +643,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
def test_build_before_save
company = companies(:first_firm)
- new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") }
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") }
assert_not_predicate company.clients_of_firm, :loaded?
company.name += "-changed"
@@ -611,7 +658,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
def test_build_many_before_save
company = companies(:first_firm)
- assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) }
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ assert_no_queries { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) }
company.name += "-changed"
assert_queries(3) { assert company.save }
@@ -620,7 +671,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
def test_build_via_block_before_save
company = companies(:first_firm)
- new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build { |client| client.name = "Another Client" } }
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ new_client = assert_no_queries { company.clients_of_firm.build { |client| client.name = "Another Client" } }
assert_not_predicate company.clients_of_firm, :loaded?
company.name += "-changed"
@@ -631,7 +686,11 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
def test_build_many_via_block_before_save
company = companies(:first_firm)
- assert_no_queries(ignore_none: false) do
+
+ # Load schema information so we don't query below if running just this test.
+ Client.define_attribute_methods
+
+ assert_no_queries do
company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) do |client|
client.name = "changed"
end
@@ -769,8 +828,9 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
assert_not_predicate @pirate, :valid?
@pirate.ship.mark_for_destruction
- @pirate.ship.expects(:valid?).never
- assert_difference("Ship.count", -1) { @pirate.save! }
+ assert_not_called(@pirate.ship, :valid?) do
+ assert_difference("Ship.count", -1) { @pirate.save! }
+ end
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice
@@ -795,7 +855,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@ship.pirate.catchphrase = "Changed Catchphrase"
@ship.name_will_change!
- assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_raise(RuntimeError) { assert_not @pirate.save }
assert_not_nil @pirate.reload.ship
end
@@ -806,8 +866,9 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
def test_should_not_save_changed_has_one_unchanged_object_if_child_is_saved
- @pirate.ship.expects(:save).never
- assert @pirate.save
+ assert_not_called(@pirate.ship, :save) do
+ assert @pirate.save
+ end
end
# belongs_to
@@ -830,8 +891,9 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
assert_not_predicate @ship, :valid?
@ship.pirate.mark_for_destruction
- @ship.pirate.expects(:valid?).never
- assert_difference("Pirate.count", -1) { @ship.save! }
+ assert_not_called(@ship.pirate, :valid?) do
+ assert_difference("Pirate.count", -1) { @ship.save! }
+ end
end
def test_a_parent_marked_for_destruction_should_not_be_destroyed_twice
@@ -855,7 +917,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@ship.pirate.catchphrase = "Changed Catchphrase"
- assert_raise(RuntimeError) { assert !@ship.save }
+ assert_raise(RuntimeError) { assert_not @ship.save }
assert_not_nil @ship.reload.pirate
end
@@ -871,7 +933,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
def test_should_destroy_has_many_as_part_of_the_save_transaction_if_they_were_marked_for_destruction
2.times { |i| @pirate.birds.create!(name: "birds_#{i}") }
- assert !@pirate.birds.any?(&:marked_for_destruction?)
+ assert_not @pirate.birds.any?(&:marked_for_destruction?)
@pirate.birds.each(&:mark_for_destruction)
klass = @pirate.birds.first.class
@@ -898,11 +960,13 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.birds.each { |bird| bird.name = "" }
assert_not_predicate @pirate, :valid?
- @pirate.birds.each do |bird|
- bird.mark_for_destruction
- bird.expects(:valid?).never
+ @pirate.birds.each(&:mark_for_destruction)
+
+ assert_not_called(@pirate.birds.first, :valid?) do
+ assert_not_called(@pirate.birds.last, :valid?) do
+ assert_difference("Bird.count", -2) { @pirate.save! }
+ end
end
- assert_difference("Bird.count", -2) { @pirate.save! }
end
def test_should_skip_validation_on_has_many_if_destroyed
@@ -921,8 +985,11 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.birds.each(&:mark_for_destruction)
assert @pirate.save
- @pirate.birds.each { |bird| bird.expects(:destroy).never }
- assert @pirate.save
+ @pirate.birds.each do |bird|
+ assert_not_called(bird, :destroy) do
+ assert @pirate.save
+ end
+ end
end
def test_should_rollback_destructions_if_an_exception_occurred_while_saving_has_many
@@ -937,7 +1004,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
end
- assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_raise(RuntimeError) { assert_not @pirate.save }
assert_equal before, @pirate.reload.birds
end
@@ -1003,7 +1070,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
def test_should_destroy_habtm_as_part_of_the_save_transaction_if_they_were_marked_for_destruction
2.times { |i| @pirate.parrots.create!(name: "parrots_#{i}") }
- assert !@pirate.parrots.any?(&:marked_for_destruction?)
+ assert_not @pirate.parrots.any?(&:marked_for_destruction?)
@pirate.parrots.each(&:mark_for_destruction)
assert_no_difference "Parrot.count" do
@@ -1022,12 +1089,14 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.parrots.each { |parrot| parrot.name = "" }
assert_not_predicate @pirate, :valid?
- @pirate.parrots.each do |parrot|
- parrot.mark_for_destruction
- parrot.expects(:valid?).never
+ @pirate.parrots.each { |parrot| parrot.mark_for_destruction }
+
+ assert_not_called(@pirate.parrots.first, :valid?) do
+ assert_not_called(@pirate.parrots.last, :valid?) do
+ @pirate.save!
+ end
end
- @pirate.save!
assert_empty @pirate.reload.parrots
end
@@ -1048,7 +1117,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
assert @pirate.save
Pirate.transaction do
- assert_queries(0) do
+ assert_no_queries do
assert @pirate.save
end
end
@@ -1065,7 +1134,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
end
end
- assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_raise(RuntimeError) { assert_not @pirate.save }
assert_equal before, @pirate.reload.parrots
end
@@ -1129,12 +1198,12 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
def test_changed_for_autosave_should_handle_cycles
@ship.pirate = @pirate
- assert_queries(0) { @ship.save! }
+ assert_no_queries { @ship.save! }
@parrot = @pirate.parrots.create(name: "some_name")
@parrot.name = "changed_name"
assert_queries(1) { @ship.save! }
- assert_queries(0) { @ship.save! }
+ assert_no_queries { @ship.save! }
end
def test_should_automatically_save_bang_the_associated_model
@@ -1213,7 +1282,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
assert_no_difference "Pirate.count" do
assert_no_difference "Ship.count" do
- assert !pirate.save
+ assert_not pirate.save
end
end
end
@@ -1232,7 +1301,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
end
end
- assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_raise(RuntimeError) { assert_not @pirate.save }
assert_equal before, [@pirate.reload.catchphrase, @pirate.ship.name]
end
@@ -1251,21 +1320,45 @@ end
class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCase
self.use_transactional_tests = false unless supports_savepoints?
- def setup
- super
+ def create_member_with_organization
organization = Organization.create
- @member = Member.create
- MemberDetail.create(organization: organization, member: @member)
+ member = Member.create
+ MemberDetail.create(organization: organization, member: member)
+
+ member
end
def test_should_not_has_one_through_model
- class << @member.organization
+ member = create_member_with_organization
+
+ class << member.organization
def save(*args)
super
raise "Oh noes!"
end
end
- assert_nothing_raised { @member.save }
+ assert_nothing_raised { member.save }
+ end
+
+ def create_author_with_post_with_comment
+ Author.create! name: "David" # make comment_id not match author_id
+ author = Author.create! name: "Sergiy"
+ post = Post.create! author: author, title: "foo", body: "bar"
+ Comment.create! post: post, body: "cool comment"
+
+ author
+ end
+
+ def test_should_not_reversed_has_one_through_model
+ author = create_author_with_post_with_comment
+
+ class << author.comment_on_first_post
+ def save(*args)
+ super
+ raise "Oh noes!"
+ end
+ end
+ assert_nothing_raised { author.save }
end
end
@@ -1337,7 +1430,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
assert_no_difference "Ship.count" do
assert_no_difference "Pirate.count" do
- assert !ship.save
+ assert_not ship.save
end
end
end
@@ -1356,7 +1449,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
end
end
- assert_raise(RuntimeError) { assert !@ship.save }
+ assert_raise(RuntimeError) { assert_not @ship.save }
assert_equal before, [@ship.pirate.reload.catchphrase, @ship.reload.name]
end
@@ -1480,7 +1573,7 @@ module AutosaveAssociationOnACollectionAssociationTests
@child_1.name = "Changed"
@child_1.cancel_save_from_callback = true
- assert !@pirate.save
+ assert_not @pirate.save
assert_equal "Don' botharrr talkin' like one, savvy?", @pirate.reload.catchphrase
assert_equal "Posideons Killer", @child_1.reload.name
@@ -1490,7 +1583,7 @@ module AutosaveAssociationOnACollectionAssociationTests
assert_no_difference "Pirate.count" do
assert_no_difference "#{new_child.class.name}.count" do
- assert !new_pirate.save
+ assert_not new_pirate.save
end
end
end
@@ -1510,7 +1603,7 @@ module AutosaveAssociationOnACollectionAssociationTests
end
end
- assert_raise(RuntimeError) { assert !@pirate.save }
+ assert_raise(RuntimeError) { assert_not @pirate.save }
assert_equal before, [@pirate.reload.catchphrase, *@pirate.send(@association_name).map(&:name)]
end
@@ -1736,3 +1829,21 @@ class TestAutosaveAssociationOnAHasManyAssociationWithInverse < ActiveRecord::Te
assert_equal 1, comment.post_comments_count
end
end
+
+class TestAutosaveAssociationOnAHasManyAssociationDefinedInSubclassWithAcceptsNestedAttributes < ActiveRecord::TestCase
+ def test_should_update_children_when_association_redefined_in_subclass
+ agency = Agency.create!(name: "Agency")
+ valid_project = Project.create!(firm: agency, name: "Initial")
+ agency.update!(
+ "projects_attributes" => {
+ "0" => {
+ "name" => "Updated",
+ "id" => valid_project.id
+ }
+ }
+ )
+ valid_project.reload
+
+ assert_equal "Updated", valid_project.name
+ end
+end
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index d5e1c2feb7..99f47cfe37 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -14,7 +14,6 @@ require "models/computer"
require "models/project"
require "models/default"
require "models/auto_id"
-require "models/boolean"
require "models/column_name"
require "models/subscriber"
require "models/comment"
@@ -283,11 +282,13 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_initialize_with_invalid_attribute
- Topic.new("title" => "test",
- "last_read(1i)" => "2005", "last_read(2i)" => "2", "last_read(3i)" => "31")
- rescue ActiveRecord::MultiparameterAssignmentErrors => ex
+ ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do
+ Topic.new("title" => "test",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00")
+ end
+
assert_equal(1, ex.errors.size)
- assert_equal("last_read", ex.errors[0].attribute)
+ assert_equal("written_on", ex.errors[0].attribute)
end
def test_create_after_initialize_without_block
@@ -307,7 +308,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal "Dude", cbs[0].name
assert_equal "Bob", cbs[1].name
assert cbs[0].frickinawesome
- assert !cbs[1].frickinawesome
+ assert_not cbs[1].frickinawesome
end
def test_load
@@ -437,12 +438,6 @@ class BasicsTest < ActiveRecord::TestCase
Post.reset_table_name
end
- if current_adapter?(:Mysql2Adapter)
- def test_update_all_with_order_and_limit
- assert_equal 1, Topic.limit(1).order("id DESC").update_all(content: "bulk updated!")
- end
- end
-
def test_null_fields
assert_nil Topic.find(1).parent_id
assert_nil Topic.create("title" => "Hey you").parent_id
@@ -690,6 +685,9 @@ class BasicsTest < ActiveRecord::TestCase
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time
+
+ topic.save!
+ assert_equal topic, Topic.find_by(attributes)
end
end
@@ -716,48 +714,6 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal expected_attributes, category.attributes
end
- def test_boolean
- b_nil = Boolean.create("value" => nil)
- nil_id = b_nil.id
- b_false = Boolean.create("value" => false)
- false_id = b_false.id
- b_true = Boolean.create("value" => true)
- true_id = b_true.id
-
- b_nil = Boolean.find(nil_id)
- assert_nil b_nil.value
- b_false = Boolean.find(false_id)
- assert_not_predicate b_false, :value?
- b_true = Boolean.find(true_id)
- assert_predicate b_true, :value?
- end
-
- def test_boolean_without_questionmark
- b_true = Boolean.create("value" => true)
- true_id = b_true.id
-
- subclass = Class.new(Boolean).find true_id
- superclass = Boolean.find true_id
-
- assert_equal superclass.read_attribute(:has_fun), subclass.read_attribute(:has_fun)
- end
-
- def test_boolean_cast_from_string
- b_blank = Boolean.create("value" => "")
- blank_id = b_blank.id
- b_false = Boolean.create("value" => "0")
- false_id = b_false.id
- b_true = Boolean.create("value" => "1")
- true_id = b_true.id
-
- b_blank = Boolean.find(blank_id)
- assert_nil b_blank.value
- b_false = Boolean.find(false_id)
- assert_not_predicate b_false, :value?
- b_true = Boolean.find(true_id)
- assert_predicate b_true, :value?
- end
-
def test_new_record_returns_boolean
assert_equal false, Topic.new.persisted?
assert_equal true, Topic.find(1).persisted?
@@ -856,11 +812,11 @@ class BasicsTest < ActiveRecord::TestCase
def test_clone_of_new_object_marks_as_dirty_only_changed_attributes
developer = Developer.new name: "Bjorn"
assert developer.name_changed? # obviously
- assert !developer.salary_changed? # attribute has non-nil default value, so treated as not changed
+ assert_not developer.salary_changed? # attribute has non-nil default value, so treated as not changed
cloned_developer = developer.clone
assert_predicate cloned_developer, :name_changed?
- assert !cloned_developer.salary_changed? # ... and cloned instance should behave same
+ assert_not cloned_developer.salary_changed? # ... and cloned instance should behave same
end
def test_dup_of_saved_object_marks_attributes_as_dirty
@@ -875,12 +831,12 @@ class BasicsTest < ActiveRecord::TestCase
def test_dup_of_saved_object_marks_as_dirty_only_changed_attributes
developer = Developer.create! name: "Bjorn"
- assert !developer.name_changed? # both attributes of saved object should be treated as not changed
+ assert_not developer.name_changed? # both attributes of saved object should be treated as not changed
assert_not_predicate developer, :salary_changed?
cloned_developer = developer.dup
assert cloned_developer.name_changed? # ... but on cloned object should be
- assert !cloned_developer.salary_changed? # ... BUT salary has non-nil default which should be treated as not changed on cloned instance
+ assert_not cloned_developer.salary_changed? # ... BUT salary has non-nil default which should be treated as not changed on cloned instance
end
def test_bignum
@@ -896,8 +852,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal company, Company.find(company.id)
end
- # TODO: extend defaults tests to other databases!
- if current_adapter?(:PostgreSQLAdapter)
+ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter, :SQLite3Adapter)
def test_default
with_timezone_config default: :local do
default = Default.new
@@ -909,7 +864,10 @@ class BasicsTest < ActiveRecord::TestCase
# char types
assert_equal "Y", default.char1
assert_equal "a varchar field", default.char2
- assert_equal "a text field", default.char3
+ # Mysql text type can't have default value
+ unless current_adapter?(:Mysql2Adapter)
+ assert_equal "a text field", default.char3
+ end
end
end
end
@@ -982,7 +940,7 @@ class BasicsTest < ActiveRecord::TestCase
end
end
- def test_clear_cash_when_setting_table_name
+ def test_clear_cache_when_setting_table_name
original_table_name = Joke.table_name
Joke.table_name = "funny_jokes"
@@ -1077,11 +1035,6 @@ class BasicsTest < ActiveRecord::TestCase
end
end
- def test_find_last
- last = Developer.last
- assert_equal last, Developer.all.merge!(order: "id desc").first
- end
-
def test_last
assert_equal Developer.all.merge!(order: "id desc").first, Developer.last
end
@@ -1097,23 +1050,23 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_find_ordered_last
- last = Developer.all.merge!(order: "developers.salary ASC").last
- assert_equal last, Developer.all.merge!(order: "developers.salary ASC").to_a.last
+ last = Developer.order("developers.salary ASC").last
+ assert_equal last, Developer.order("developers.salary": "ASC").to_a.last
end
def test_find_reverse_ordered_last
- last = Developer.all.merge!(order: "developers.salary DESC").last
- assert_equal last, Developer.all.merge!(order: "developers.salary DESC").to_a.last
+ last = Developer.order("developers.salary DESC").last
+ assert_equal last, Developer.order("developers.salary": "DESC").to_a.last
end
def test_find_multiple_ordered_last
- last = Developer.all.merge!(order: "developers.name, developers.salary DESC").last
- assert_equal last, Developer.all.merge!(order: "developers.name, developers.salary DESC").to_a.last
+ last = Developer.order("developers.name, developers.salary DESC").last
+ assert_equal last, Developer.order(:"developers.name", "developers.salary": "DESC").to_a.last
end
def test_find_keeps_multiple_order_values
- combined = Developer.all.merge!(order: "developers.name, developers.salary").to_a
- assert_equal combined, Developer.all.merge!(order: ["developers.name", "developers.salary"]).to_a
+ combined = Developer.order("developers.name, developers.salary").to_a
+ assert_equal combined, Developer.order(:"developers.name", :"developers.salary").to_a
end
def test_find_keeps_multiple_group_values
@@ -1265,14 +1218,15 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_attribute_names
- assert_equal ["id", "type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description"],
- Company.attribute_names
+ expected = ["id", "type", "firm_id", "firm_name", "name", "client_of", "rating", "account_id", "description", "metadata"]
+ assert_equal expected, Company.attribute_names
end
def test_has_attribute
assert Company.has_attribute?("id")
assert Company.has_attribute?("type")
assert Company.has_attribute?("name")
+ assert Company.has_attribute?("metadata")
assert_not Company.has_attribute?("lastname")
assert_not Company.has_attribute?("age")
end
@@ -1486,6 +1440,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
@@ -1519,4 +1481,64 @@ class BasicsTest < ActiveRecord::TestCase
ensure
ActiveRecord::Base.protected_environments = previous_protected_environments
end
+
+ test "creating a record raises if preventing writes" do
+ error = assert_raises ActiveRecord::ReadOnlyError do
+ ActiveRecord::Base.connection.while_preventing_writes do
+ Bird.create! name: "Bluejay"
+ end
+ end
+
+ assert_match %r/\AWrite query attempted while in readonly mode: INSERT /, error.message
+ end
+
+ test "updating a record raises if preventing writes" do
+ bird = Bird.create! name: "Bluejay"
+
+ error = assert_raises ActiveRecord::ReadOnlyError do
+ ActiveRecord::Base.connection.while_preventing_writes do
+ bird.update! name: "Robin"
+ end
+ end
+
+ assert_match %r/\AWrite query attempted while in readonly mode: UPDATE /, error.message
+ end
+
+ test "deleting a record raises if preventing writes" do
+ bird = Bird.create! name: "Bluejay"
+
+ error = assert_raises ActiveRecord::ReadOnlyError do
+ ActiveRecord::Base.connection.while_preventing_writes do
+ bird.destroy!
+ end
+ end
+
+ assert_match %r/\AWrite query attempted while in readonly mode: DELETE /, error.message
+ 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
+
+ test "an explain query does not raise if preventing writes" do
+ Bird.create!(name: "Bluejay")
+
+ ActiveRecord::Base.connection.while_preventing_writes do
+ assert_queries(2) { Bird.where(name: "Bluejay").explain }
+ end
+ end
+
+ test "an empty transaction does not raise if preventing writes" do
+ ActiveRecord::Base.connection.while_preventing_writes do
+ assert_queries(2, ignore_none: true) do
+ Bird.transaction do
+ ActiveRecord::Base.connection.materialize_transactions
+ end
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index c8163901c6..cf6e280898 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -24,7 +24,7 @@ class EachTest < ActiveRecord::TestCase
def test_each_should_not_return_query_chain_and_execute_only_one_query
assert_queries(1) do
- result = Post.find_each(batch_size: 100000) {}
+ result = Post.find_each(batch_size: 100000) { }
assert_nil result
end
end
@@ -155,7 +155,7 @@ class EachTest < ActiveRecord::TestCase
end
def test_find_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified
- not_a_post = "not a post".dup
+ not_a_post = +"not a post"
def not_a_post.id; end
not_a_post.stub(:id, -> { raise StandardError.new("not_a_post had #id called on it") }) do
assert_nothing_raised do
@@ -183,7 +183,7 @@ class EachTest < ActiveRecord::TestCase
def test_find_in_batches_should_error_on_ignore_the_order
assert_raise(ArgumentError) do
- PostWithDefaultScope.find_in_batches(error_on_ignore: true) {}
+ PostWithDefaultScope.find_in_batches(error_on_ignore: true) { }
end
end
@@ -192,7 +192,7 @@ class EachTest < ActiveRecord::TestCase
prev = ActiveRecord::Base.error_on_ignored_order
ActiveRecord::Base.error_on_ignored_order = true
assert_nothing_raised do
- PostWithDefaultScope.find_in_batches(error_on_ignore: false) {}
+ PostWithDefaultScope.find_in_batches(error_on_ignore: false) { }
end
ensure
# Set back to default
@@ -204,7 +204,7 @@ class EachTest < ActiveRecord::TestCase
prev = ActiveRecord::Base.error_on_ignored_order
ActiveRecord::Base.error_on_ignored_order = true
assert_raise(ArgumentError) do
- PostWithDefaultScope.find_in_batches() {}
+ PostWithDefaultScope.find_in_batches() { }
end
ensure
# Set back to default
@@ -213,7 +213,7 @@ class EachTest < ActiveRecord::TestCase
def test_find_in_batches_should_not_error_by_default
assert_nothing_raised do
- PostWithDefaultScope.find_in_batches() {}
+ PostWithDefaultScope.find_in_batches() { }
end
end
@@ -228,7 +228,7 @@ class EachTest < ActiveRecord::TestCase
def test_find_in_batches_should_not_modify_passed_options
assert_nothing_raised do
- Post.find_in_batches({ batch_size: 42, start: 1 }.freeze) {}
+ Post.find_in_batches({ batch_size: 42, start: 1 }.freeze) { }
end
end
@@ -420,7 +420,7 @@ class EachTest < ActiveRecord::TestCase
end
def test_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified
- not_a_post = "not a post".dup
+ not_a_post = +"not a post"
def not_a_post.id
raise StandardError.new("not_a_post had #id called on it")
end
@@ -430,7 +430,7 @@ class EachTest < ActiveRecord::TestCase
assert_kind_of ActiveRecord::Relation, relation
assert_kind_of Post, relation.first
- relation = [not_a_post] * relation.count
+ [not_a_post] * relation.count
end
end
end
@@ -446,7 +446,7 @@ class EachTest < ActiveRecord::TestCase
def test_in_batches_should_not_modify_passed_options
assert_nothing_raised do
- Post.in_batches({ of: 42, start: 1 }.freeze) {}
+ Post.in_batches({ of: 42, start: 1 }.freeze) { }
end
end
@@ -597,15 +597,15 @@ class EachTest < ActiveRecord::TestCase
table: table_alias,
predicate_builder: predicate_builder
)
- posts.find_each {}
+ posts.find_each { }
end
end
test ".find_each bypasses the query cache for its own queries" do
Post.cache do
assert_queries(2) do
- Post.find_each {}
- Post.find_each {}
+ Post.find_each { }
+ Post.find_each { }
end
end
end
@@ -624,8 +624,8 @@ class EachTest < ActiveRecord::TestCase
test ".find_in_batches bypasses the query cache for its own queries" do
Post.cache do
assert_queries(2) do
- Post.find_in_batches {}
- Post.find_in_batches {}
+ Post.find_in_batches { }
+ Post.find_in_batches { }
end
end
end
@@ -644,8 +644,8 @@ class EachTest < ActiveRecord::TestCase
test ".in_batches bypasses the query cache for its own queries" do
Post.cache do
assert_queries(2) do
- Post.in_batches {}
- Post.in_batches {}
+ Post.in_batches { }
+ Post.in_batches { }
end
end
end
diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb
index d5376ece69..58abdb47f7 100644
--- a/activerecord/test/cases/binary_test.rb
+++ b/activerecord/test/cases/binary_test.rb
@@ -12,7 +12,7 @@ unless current_adapter?(:DB2Adapter)
FIXTURES = %w(flowers.jpg example.log test.txt)
def test_mixed_encoding
- str = "\x80".dup
+ str = +"\x80"
str.force_encoding("ASCII-8BIT")
binary = Binary.new name: "いただきます!", data: str
diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb
index 91cc49385c..85685d1d00 100644
--- a/activerecord/test/cases/bind_parameter_test.rb
+++ b/activerecord/test/cases/bind_parameter_test.rb
@@ -2,6 +2,7 @@
require "cases/helper"
require "models/topic"
+require "models/reply"
require "models/author"
require "models/post"
@@ -34,6 +35,100 @@ if ActiveRecord::Base.connection.prepared_statements
ActiveSupport::Notifications.unsubscribe(@subscription)
end
+ def test_statement_cache
+ @connection.clear_cache!
+
+ topics = Topic.where(id: 1)
+ assert_equal [1], topics.map(&:id)
+ assert_includes statement_cache, to_sql_key(topics.arel)
+
+ @connection.clear_cache!
+
+ assert_not_includes statement_cache, to_sql_key(topics.arel)
+ end
+
+ def test_statement_cache_with_query_cache
+ @connection.enable_query_cache!
+ @connection.clear_cache!
+
+ topics = Topic.where(id: 1)
+ assert_equal [1], topics.map(&:id)
+ assert_includes statement_cache, to_sql_key(topics.arel)
+ ensure
+ @connection.disable_query_cache!
+ end
+
+ def test_statement_cache_with_find
+ @connection.clear_cache!
+
+ assert_equal 1, Topic.find(1).id
+ assert_raises(RecordNotFound) { SillyReply.find(2) }
+
+ topic_sql = cached_statement(Topic, Topic.primary_key)
+ assert_includes statement_cache, to_sql_key(topic_sql)
+
+ e = assert_raise { cached_statement(SillyReply, SillyReply.primary_key) }
+ assert_equal "SillyReply has no cached statement by \"id\"", e.message
+
+ replies = SillyReply.where(id: 2).limit(1)
+ assert_includes statement_cache, to_sql_key(replies.arel)
+ end
+
+ def test_statement_cache_with_find_by
+ @connection.clear_cache!
+
+ assert_equal 1, Topic.find_by!(id: 1).id
+ assert_raises(RecordNotFound) { SillyReply.find_by!(id: 2) }
+
+ topic_sql = cached_statement(Topic, [:id])
+ assert_includes statement_cache, to_sql_key(topic_sql)
+
+ e = assert_raise { cached_statement(SillyReply, [:id]) }
+ assert_equal "SillyReply has no cached statement by [:id]", e.message
+
+ replies = SillyReply.where(id: 2).limit(1)
+ assert_includes statement_cache, to_sql_key(replies.arel)
+ end
+
+ def test_statement_cache_with_in_clause
+ @connection.clear_cache!
+
+ topics = Topic.where(id: [1, 3])
+ assert_equal [1, 3], topics.map(&:id)
+ assert_not_includes statement_cache, to_sql_key(topics.arel)
+ end
+
+ def test_statement_cache_with_sql_string_literal
+ @connection.clear_cache!
+
+ topics = Topic.where("topics.id = ?", 1)
+ assert_equal [1], topics.map(&:id)
+ assert_not_includes statement_cache, to_sql_key(topics.arel)
+ end
+
+ def test_too_many_binds
+ bind_params_length = @connection.send(:bind_params_length)
+
+ 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_too_many_binds_with_query_cache
+ @connection.enable_query_cache!
+
+ bind_params_length = @connection.send(:bind_params_length)
+ topics = Topic.where(id: (1 .. bind_params_length + 1).to_a)
+ assert_equal Topic.count, topics.count
+
+ topics = Topic.where.not(id: (1 .. bind_params_length + 1).to_a)
+ assert_equal 0, topics.count
+ ensure
+ @connection.disable_query_cache!
+ end
+
def test_bind_from_join_in_subquery
subquery = Author.joins(:thinking_posts).where(name: "David")
scope = Author.from(subquery, "authors").where(id: 1)
@@ -67,17 +162,29 @@ if ActiveRecord::Base.connection.prepared_statements
assert_logs_binds(binds)
end
- def test_deprecate_supports_statement_cache
- assert_deprecated { ActiveRecord::Base.connection.supports_statement_cache? }
- end
-
private
+ def to_sql_key(arel)
+ sql = @connection.to_sql(arel)
+ @connection.respond_to?(:sql_key, true) ? @connection.send(:sql_key, sql) : sql
+ end
+
+ def cached_statement(klass, key)
+ cache = klass.send(:cached_find_by_statement, key) do
+ raise "#{klass} has no cached statement by #{key.inspect}"
+ end
+ cache.send(:query_builder).instance_variable_get(:@sql)
+ end
+
+ def statement_cache
+ @connection.instance_variable_get(:@statements).send(:cache)
+ end
+
def assert_logs_binds(binds)
payload = {
name: "SQL",
sql: "select * from topics where id = ?",
binds: binds,
- type_casted_binds: @connection.type_casted_binds(binds)
+ type_casted_binds: @connection.send(:type_casted_binds, binds)
}
event = ActiveSupport::Notifications::Event.new(
diff --git a/activerecord/test/cases/boolean_test.rb b/activerecord/test/cases/boolean_test.rb
new file mode 100644
index 0000000000..18824004d2
--- /dev/null
+++ b/activerecord/test/cases/boolean_test.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/boolean"
+
+class BooleanTest < ActiveRecord::TestCase
+ def test_boolean
+ b_nil = Boolean.create!(value: nil)
+ b_false = Boolean.create!(value: false)
+ b_true = Boolean.create!(value: true)
+
+ assert_nil Boolean.find(b_nil.id).value
+ assert_not_predicate Boolean.find(b_false.id), :value?
+ assert_predicate Boolean.find(b_true.id), :value?
+ end
+
+ def test_boolean_without_questionmark
+ b_true = Boolean.create!(value: true)
+
+ subclass = Class.new(Boolean).find(b_true.id)
+ superclass = Boolean.find(b_true.id)
+
+ assert_equal superclass.read_attribute(:has_fun), subclass.read_attribute(:has_fun)
+ end
+
+ def test_boolean_cast_from_string
+ b_blank = Boolean.create!(value: "")
+ b_false = Boolean.create!(value: "0")
+ b_true = Boolean.create!(value: "1")
+
+ assert_nil Boolean.find(b_blank.id).value
+ assert_not_predicate Boolean.find(b_false.id), :value?
+ assert_predicate Boolean.find(b_true.id), :value?
+ end
+
+ def test_find_by_boolean_string
+ b_false = Boolean.create!(value: "false")
+ b_true = Boolean.create!(value: "true")
+
+ assert_equal b_false, Boolean.find_by(value: "false")
+ assert_equal b_true, Boolean.find_by(value: "true")
+ end
+
+ def test_find_by_falsy_boolean_symbol
+ ActiveModel::Type::Boolean::FALSE_VALUES.each do |value|
+ b_false = Boolean.create!(value: value)
+
+ assert_not_predicate b_false, :value?
+ assert_equal b_false, Boolean.find_by(id: b_false.id, value: value.to_s.to_sym)
+ end
+ end
+end
diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb
index 3a569f226e..c27eb8a65d 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 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
+
+ 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 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
+
+ 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 and PostgreSQL don't return a string value for updated_at") if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
+
+ 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 080d2a54bc..16c2a3661d 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -19,6 +19,7 @@ require "models/developer"
require "models/post"
require "models/comment"
require "models/rating"
+require "support/stubs/strong_parameters"
class CalculationsTest < ActiveRecord::TestCase
fixtures :companies, :accounts, :topics, :speedometers, :minivans, :books, :posts, :comments
@@ -218,8 +219,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
@@ -242,6 +243,12 @@ class CalculationsTest < ActiveRecord::TestCase
assert_queries(1) { assert_equal 11, posts.count(:all) }
end
+ def test_count_with_eager_loading_and_custom_select_and_order
+ posts = Post.includes(:comments).order("comments.id").select(:type)
+ assert_queries(1) { assert_equal 11, posts.count }
+ assert_queries(1) { assert_equal 11, posts.count(:all) }
+ end
+
def test_count_with_eager_loading_and_custom_order_and_distinct
posts = Post.includes(:comments).order("comments.id").distinct
assert_queries(1) { assert_equal 11, posts.count }
@@ -278,6 +285,18 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 3, Account.joins(:firm).distinct.order(:firm_id).limit(3).offset(2).count
end
+ def test_distinct_joins_count_with_group_by
+ expected = { nil => 4, 1 => 1, 2 => 1, 4 => 1, 5 => 1, 7 => 1 }
+ assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.count(:author_id)
+ assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.select(:author_id).count
+ assert_equal expected, Post.left_joins(:comments).group(:post_id).count("DISTINCT posts.author_id")
+ assert_equal expected, Post.left_joins(:comments).group(:post_id).select("DISTINCT posts.author_id").count
+
+ expected = { nil => 6, 1 => 1, 2 => 1, 4 => 1, 5 => 1, 7 => 1 }
+ assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.count(:all)
+ assert_equal expected, Post.left_joins(:comments).group(:post_id).distinct.select(:author_id).count(:all)
+ end
+
def test_distinct_count_with_group_by_and_order_and_limit
assert_equal({ 6 => 2 }, Account.group(:firm_id).distinct.order("1 DESC").limit(1).count)
end
@@ -344,6 +363,17 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 60, c[2]
end
+ def test_should_calculate_grouped_with_longer_field
+ field = "a" * Account.connection.max_identifier_length
+
+ Account.update_all("#{field} = credit_limit")
+
+ c = Account.group(:firm_id).sum(field)
+ assert_equal 50, c[1]
+ assert_equal 105, c[6]
+ assert_equal 60, c[2]
+ end
+
def test_should_calculate_with_invalid_field
assert_equal 6, Account.calculate(:count, "*")
assert_equal 6, Account.calculate(:count, :all)
@@ -428,6 +458,8 @@ class CalculationsTest < ActiveRecord::TestCase
def test_should_count_selected_field_with_include
assert_equal 6, Account.includes(:firm).distinct.count
assert_equal 4, Account.includes(:firm).distinct.select(:credit_limit).count
+ assert_equal 4, Account.includes(:firm).distinct.count("DISTINCT credit_limit")
+ assert_equal 4, Account.includes(:firm).distinct.count("DISTINCT(credit_limit)")
end
def test_should_not_perform_joined_include_by_default
@@ -458,6 +490,24 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 6, Account.select("DISTINCT accounts.id").includes(:firm).count
end
+ def test_should_count_manual_select_with_count_all
+ assert_equal 5, Account.select("DISTINCT accounts.firm_id").count(:all)
+ end
+
+ def test_should_count_with_manual_distinct_select_and_distinct
+ assert_equal 4, Account.select("DISTINCT accounts.firm_id").distinct(true).count
+ end
+
+ def test_should_count_manual_select_with_group_with_count_all
+ expected = { nil => 1, 1 => 1, 2 => 1, 6 => 2, 9 => 1 }
+ actual = Account.select("DISTINCT accounts.firm_id").group("accounts.firm_id").count(:all)
+ assert_equal expected, actual
+ end
+
+ def test_should_count_manual_with_count_all
+ assert_equal 6, Account.count(:all)
+ end
+
def test_count_selected_arel_attribute
assert_equal 5, Account.select(Account.arel_table[:firm_id]).count
assert_equal 4, Account.distinct.select(Account.arel_table[:firm_id]).count
@@ -509,8 +559,10 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_count_field_of_root_table_with_conflicting_group_by_column
- assert_equal({ 1 => 1 }, Firm.joins(:accounts).group(:firm_id).count)
- assert_equal({ 1 => 1 }, Firm.joins(:accounts).group("accounts.firm_id").count)
+ expected = { 1 => 2, 2 => 1, 4 => 5, 5 => 2, 7 => 1 }
+ assert_equal expected, Post.joins(:comments).group(:post_id).count
+ assert_equal expected, Post.joins(:comments).group("comments.post_id").count
+ assert_equal expected, Post.joins(:comments).group(:post_id).select("DISTINCT posts.author_id").count(:all)
end
def test_count_with_no_parameters_isnt_deprecated
@@ -642,6 +694,18 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal [ topic.written_on ], relation.pluck(:written_on)
end
+ def test_pluck_with_type_cast_does_not_corrupt_the_query_cache
+ topic = topics(:first)
+ relation = Topic.where(id: topic.id)
+ assert_queries 1 do
+ Topic.cache do
+ kind = relation.select(:written_on).load.first.read_attribute_before_type_cast(:written_on).class
+ relation.pluck(:written_on)
+ assert_kind_of kind, relation.select(:written_on).load.first.read_attribute_before_type_cast(:written_on)
+ end
+ end
+ end
+
def test_pluck_and_distinct
assert_equal [50, 53, 55, 60], Account.order(:credit_limit).distinct.pluck(:credit_limit)
end
@@ -676,8 +740,9 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_pluck_not_auto_table_name_prefix_if_column_joined
- Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
- assert_equal [7], Company.joins(:contracts).pluck(:developer_id)
+ company = Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
+ metadata = company.contracts.first.metadata
+ assert_equal [metadata], Company.joins(:contracts).pluck(:metadata)
end
def test_pluck_with_selection_clause
@@ -705,6 +770,28 @@ 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")
+ assert_equal expected, actual
+ end
+
+ def test_group_by_with_offset
+ expected = { "SpecialPost" => 1, "StiPost" => 2 }
+ actual = Post.includes(:comments).group(:type).order(:type).offset(1).count("comments.id")
+ assert_equal expected, actual
+ end
+
+ def test_group_by_with_limit_and_offset
+ expected = { "SpecialPost" => 1 }
+ actual = Post.includes(:comments).group(:type).order(:type).offset(1).limit(1).count("comments.id")
+ assert_equal expected, actual
+ end
+
def test_pluck_not_auto_table_name_prefix_if_column_included
Company.create!(name: "test", contracts: [Contract.new(developer_id: 7)])
ids = Company.includes(:contracts).pluck(:developer_id)
@@ -802,13 +889,13 @@ class CalculationsTest < ActiveRecord::TestCase
def test_pick_one
assert_equal "The First Topic", Topic.order(:id).pick(:heading)
assert_nil Topic.none.pick(:heading)
- assert_nil Topic.where("1=0").pick(:heading)
+ assert_nil Topic.where(id: 9999999999999999999).pick(:heading)
end
def test_pick_two
assert_equal ["David", "david@loudthinking.com"], Topic.order(:id).pick(:author_name, :author_email_address)
assert_nil Topic.none.pick(:author_name, :author_email_address)
- assert_nil Topic.where("1=0").pick(:author_name, :author_email_address)
+ assert_nil Topic.where(id: 9999999999999999999).pick(:author_name, :author_email_address)
end
def test_pick_delegate_to_all
@@ -846,26 +933,7 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_having_with_strong_parameters
- protected_params = Class.new do
- attr_reader :permitted
- alias :permitted? :permitted
-
- def initialize(parameters)
- @parameters = parameters
- @permitted = false
- end
-
- def to_h
- @parameters
- end
-
- def permit!
- @permitted = true
- self
- end
- end
-
- params = protected_params.new(credit_limit: "50")
+ params = ProtectedParams.new(credit_limit: "50")
assert_raises(ActiveModel::ForbiddenAttributesError) do
Account.group(:id).having(params)
@@ -881,15 +949,15 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal({ "proposed" => 2, "published" => 2 }, Book.group(:status).count)
end
- def test_deprecate_count_with_block_and_column_name
- assert_deprecated do
- assert_equal 6, Account.count(:firm_id) { true }
+ def test_count_with_block_and_column_name_raises_an_error
+ assert_raises(ArgumentError) do
+ Account.count(:firm_id) { true }
end
end
- def test_deprecate_sum_with_block_and_column_name
- assert_deprecated do
- assert_equal 6, Account.sum(:firm_id) { 1 }
+ def test_sum_with_block_and_column_name_raises_an_error
+ assert_raises(ArgumentError) do
+ Account.sum(:firm_id) { 1 }
end
end
diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb
index 3b283a3aa6..4d6a112af5 100644
--- a/activerecord/test/cases/callbacks_test.rb
+++ b/activerecord/test/cases/callbacks_test.rb
@@ -21,7 +21,7 @@ class CallbackDeveloper < ActiveRecord::Base
def callback_object(callback_method)
klass = Class.new
- klass.send(:define_method, callback_method) do |model|
+ klass.define_method(callback_method) do |model|
model.history << [callback_method, :object]
end
klass.new
@@ -385,9 +385,9 @@ class CallbacksTest < ActiveRecord::TestCase
end
def assert_save_callbacks_not_called(someone)
- assert !someone.after_save_called
- assert !someone.after_create_called
- assert !someone.after_update_called
+ assert_not someone.after_save_called
+ assert_not someone.after_create_called
+ assert_not someone.after_update_called
end
private :assert_save_callbacks_not_called
@@ -395,27 +395,27 @@ class CallbacksTest < ActiveRecord::TestCase
someone = CallbackHaltedDeveloper.new
someone.cancel_before_create = true
assert_predicate someone, :valid?
- assert !someone.save
+ assert_not someone.save
assert_save_callbacks_not_called(someone)
end
def test_before_save_throwing_abort
david = DeveloperWithCanceledCallbacks.find(1)
assert_predicate david, :valid?
- assert !david.save
+ assert_not david.save
exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! }
assert_equal david, exc.record
david = DeveloperWithCanceledCallbacks.find(1)
david.salary = 10_000_000
assert_not_predicate david, :valid?
- assert !david.save
+ assert_not david.save
assert_raise(ActiveRecord::RecordInvalid) { david.save! }
someone = CallbackHaltedDeveloper.find(1)
someone.cancel_before_save = true
assert_predicate someone, :valid?
- assert !someone.save
+ assert_not someone.save
assert_save_callbacks_not_called(someone)
end
@@ -423,22 +423,22 @@ class CallbacksTest < ActiveRecord::TestCase
someone = CallbackHaltedDeveloper.find(1)
someone.cancel_before_update = true
assert_predicate someone, :valid?
- assert !someone.save
+ assert_not someone.save
assert_save_callbacks_not_called(someone)
end
def test_before_destroy_throwing_abort
david = DeveloperWithCanceledCallbacks.find(1)
- assert !david.destroy
+ assert_not david.destroy
exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! }
assert_equal david, exc.record
assert_not_nil ImmutableDeveloper.find_by_id(1)
someone = CallbackHaltedDeveloper.find(1)
someone.cancel_before_destroy = true
- assert !someone.destroy
+ assert_not someone.destroy
assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! }
- assert !someone.after_destroy_called
+ assert_not someone.after_destroy_called
end
def test_callback_throwing_abort
@@ -467,13 +467,40 @@ class CallbacksTest < ActiveRecord::TestCase
def test_inheritance_of_callbacks
parent = ParentDeveloper.new
- assert !parent.after_save_called
+ assert_not parent.after_save_called
parent.save
assert parent.after_save_called
child = ChildDeveloper.new
- assert !child.after_save_called
+ assert_not child.after_save_called
child.save
assert child.after_save_called
end
+
+ def test_before_save_doesnt_allow_on_option
+ exception = assert_raises ArgumentError do
+ Class.new(ActiveRecord::Base) do
+ before_save(on: :create) { }
+ end
+ end
+ assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message
+ end
+
+ def test_around_save_doesnt_allow_on_option
+ exception = assert_raises ArgumentError do
+ Class.new(ActiveRecord::Base) do
+ around_save(on: :create) { }
+ end
+ end
+ assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message
+ end
+
+ def test_after_save_doesnt_allow_on_option
+ exception = assert_raises ArgumentError do
+ Class.new(ActiveRecord::Base) do
+ after_save(on: :create) { }
+ end
+ end
+ assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message
+ end
end
diff --git a/activerecord/test/cases/clone_test.rb b/activerecord/test/cases/clone_test.rb
index 65e5016040..eea36ee736 100644
--- a/activerecord/test/cases/clone_test.rb
+++ b/activerecord/test/cases/clone_test.rb
@@ -12,7 +12,7 @@ module ActiveRecord
cloned = topic.clone
assert topic.persisted?, "topic persisted"
assert cloned.persisted?, "topic persisted"
- assert !cloned.new_record?, "topic is not new"
+ assert_not cloned.new_record?, "topic is not new"
end
def test_stays_frozen
@@ -21,7 +21,7 @@ module ActiveRecord
cloned = topic.clone
assert cloned.persisted?, "topic persisted"
- assert !cloned.new_record?, "topic is not new"
+ assert_not cloned.new_record?, "topic is not new"
assert cloned.frozen?, "topic should be frozen"
end
diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb
index a5d908344a..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
@@ -91,12 +105,12 @@ module ActiveRecord
developers = Developer.where(name: "David")
assert_queries(1) { developers.cache_key }
- assert_queries(0) { developers.cache_key }
+ assert_no_queries { developers.cache_key }
end
test "it doesn't trigger any query if the relation is already loaded" do
developers = Developer.where(name: "David").load
- assert_queries(0) { developers.cache_key }
+ assert_no_queries { developers.cache_key }
end
test "relation cache_key changes when the sql query changes" do
diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
index 57b3a5844e..6282759a10 100644
--- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
@@ -28,13 +28,16 @@ module ActiveRecord
end
def test_establish_connection_uses_spec_name
+ old_config = ActiveRecord::Base.configurations
config = { "readonly" => { "adapter" => "sqlite3" } }
- resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(config)
+ ActiveRecord::Base.configurations = config
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(ActiveRecord::Base.configurations)
spec = resolver.spec(:readonly)
@handler.establish_connection(spec.to_hash)
assert_not_nil @handler.retrieve_connection_pool("readonly")
ensure
+ ActiveRecord::Base.configurations = old_config
@handler.remove_connection("readonly")
end
@@ -89,7 +92,7 @@ module ActiveRecord
ActiveRecord::Base.establish_connection
- assert_equal "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database]
+ assert_match "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database]
ensure
ActiveRecord::Base.configurations = @prev_configs
ENV["RAILS_ENV"] = previous_env
@@ -112,7 +115,7 @@ module ActiveRecord
ActiveRecord::Base.establish_connection
- assert_equal "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database]
+ assert_match "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database]
ensure
ActiveRecord::Base.configurations = @prev_configs
ENV["RAILS_ENV"] = previous_env
@@ -148,6 +151,35 @@ module ActiveRecord
ActiveRecord::Base.configurations = @prev_configs
end
+ def test_symbolized_configurations_assignment
+ @prev_configs = ActiveRecord::Base.configurations
+ config = {
+ development: {
+ primary: {
+ adapter: "sqlite3",
+ database: "db/development.sqlite3",
+ },
+ },
+ test: {
+ primary: {
+ adapter: "sqlite3",
+ database: "db/test.sqlite3",
+ },
+ },
+ }
+ ActiveRecord::Base.configurations = config
+ ActiveRecord::Base.configurations.configs_for.each do |db_config|
+ assert_instance_of ActiveRecord::DatabaseConfigurations::HashConfig, db_config
+ assert_instance_of String, db_config.env_name
+ assert_instance_of String, db_config.spec_name
+ db_config.config.keys.each do |key|
+ assert_instance_of String, key
+ end
+ end
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ end
+
def test_retrieve_connection
assert @handler.retrieve_connection(@spec_name)
end
@@ -350,6 +382,11 @@ module ActiveRecord
assert_not_nil ActiveRecord::Base.connection
assert_same klass2.connection, ActiveRecord::Base.connection
end
+
+ def test_default_handlers_are_writing_and_reading
+ assert_equal :writing, ActiveRecord::Base.writing_role
+ assert_equal :reading, ActiveRecord::Base.reading_role
+ end
end
end
end
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
new file mode 100644
index 0000000000..36591097b6
--- /dev/null
+++ b/activerecord/test/cases/connection_adapters/connection_handlers_multi_db_test.rb
@@ -0,0 +1,391 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/person"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class ConnectionHandlersMultiDbTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ fixtures :people
+
+ def setup
+ @handlers = { writing: ConnectionHandler.new, reading: ConnectionHandler.new }
+ @rw_handler = @handlers[:writing]
+ @ro_handler = @handlers[:reading]
+ @spec_name = "primary"
+ @rw_pool = @handlers[:writing].establish_connection(ActiveRecord::Base.configurations["arunit"])
+ @ro_pool = @handlers[:reading].establish_connection(ActiveRecord::Base.configurations["arunit"])
+ end
+
+ def teardown
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
+ end
+
+ class MultiConnectionTestModel < ActiveRecord::Base
+ end
+
+ def test_multiple_connection_handlers_works_in_a_threaded_environment
+ tf_writing = Tempfile.open "test_writing"
+ tf_reading = Tempfile.open "test_reading"
+
+ MultiConnectionTestModel.connects_to database: { writing: { database: tf_writing.path, adapter: "sqlite3" }, reading: { database: tf_reading.path, adapter: "sqlite3" } }
+
+ MultiConnectionTestModel.connection.execute("CREATE TABLE `test_1` (connection_role VARCHAR (255))")
+ MultiConnectionTestModel.connection.execute("INSERT INTO test_1 VALUES ('writing')")
+
+ ActiveRecord::Base.connected_to(role: :reading) do
+ MultiConnectionTestModel.connection.execute("CREATE TABLE `test_1` (connection_role VARCHAR (255))")
+ MultiConnectionTestModel.connection.execute("INSERT INTO test_1 VALUES ('reading')")
+ end
+
+ read_latch = Concurrent::CountDownLatch.new
+ write_latch = Concurrent::CountDownLatch.new
+
+ MultiConnectionTestModel.connection
+
+ thread = Thread.new do
+ MultiConnectionTestModel.connection
+
+ write_latch.wait
+ assert_equal "writing", MultiConnectionTestModel.connection.select_value("SELECT connection_role from test_1")
+ read_latch.count_down
+ end
+
+ ActiveRecord::Base.connected_to(role: :reading) do
+ write_latch.count_down
+ assert_equal "reading", MultiConnectionTestModel.connection.select_value("SELECT connection_role from test_1")
+ read_latch.wait
+ end
+
+ thread.join
+ ensure
+ tf_reading.close
+ tf_reading.unlink
+ tf_writing.close
+ tf_writing.unlink
+ end
+
+ unless in_memory_db?
+ def test_establish_connection_using_3_levels_config
+ 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.connects_to(database: { writing: :primary, reading: :readonly })
+
+ assert_not_nil pool = ActiveRecord::Base.connection_handlers[:writing].retrieve_connection_pool("primary")
+ assert_equal "db/primary.sqlite3", pool.spec.config[:database]
+
+ assert_not_nil pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary")
+ assert_equal "db/readonly.sqlite3", pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ActiveRecord::Base.establish_connection(:arunit)
+ ENV["RAILS_ENV"] = previous_env
+ end
+
+ def test_switching_connections_via_handler
+ 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.connects_to(database: { writing: :primary, reading: :readonly })
+
+ ActiveRecord::Base.connected_to(role: :reading) do
+ @ro_handler = ActiveRecord::Base.connection_handler
+ assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:reading]
+ assert_equal :reading, ActiveRecord::Base.current_role
+ assert ActiveRecord::Base.connected_to?(role: :reading)
+ assert_not ActiveRecord::Base.connected_to?(role: :writing)
+ end
+
+ ActiveRecord::Base.connected_to(role: :writing) do
+ assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:writing]
+ assert_not_equal @ro_handler, ActiveRecord::Base.connection_handler
+ assert_equal :writing, ActiveRecord::Base.current_role
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ assert_not ActiveRecord::Base.connected_to?(role: :reading)
+ end
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ActiveRecord::Base.establish_connection(:arunit)
+ ENV["RAILS_ENV"] = previous_env
+ end
+
+ def test_establish_connection_using_3_levels_config_with_non_default_handlers
+ 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.connects_to(database: { default: :primary, readonly: :readonly })
+
+ assert_not_nil pool = ActiveRecord::Base.connection_handlers[:default].retrieve_connection_pool("primary")
+ assert_equal "db/primary.sqlite3", pool.spec.config[:database]
+
+ assert_not_nil pool = ActiveRecord::Base.connection_handlers[:readonly].retrieve_connection_pool("primary")
+ assert_equal "db/readonly.sqlite3", pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ActiveRecord::Base.establish_connection(:arunit)
+ 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
+ assert_equal :writing, ActiveRecord::Base.current_role
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+
+ 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
+ assert_equal :writing, ActiveRecord::Base.current_role
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+
+ 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
+ assert_equal :readonly, ActiveRecord::Base.current_role
+ assert ActiveRecord::Base.connected_to?(role: :readonly)
+
+ 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" },
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ ActiveRecord::Base.connects_to database: { writing: :development }
+
+ assert_equal 1, ActiveRecord::Base.connection_handlers.size
+ assert_equal ActiveRecord::Base.connection_handler, ActiveRecord::Base.connection_handlers[:writing]
+ assert_equal :writing, ActiveRecord::Base.current_role
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ActiveRecord::Base.establish_connection(:arunit)
+ end
+
+ def test_connects_to_using_top_level_key_in_two_level_config
+ 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
+
+ ActiveRecord::Base.connects_to database: { writing: :development, reading: :development_readonly }
+
+ assert_not_nil pool = ActiveRecord::Base.connection_handlers[:reading].retrieve_connection_pool("primary")
+ assert_equal "db/readonly.sqlite3", pool.spec.config[:database]
+ ensure
+ 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
+ assert_equal([@rw_pool], @handlers[:writing].connection_pools)
+ assert_equal([@ro_pool], @handlers[:reading].connection_pools)
+ end
+
+ def test_retrieve_connection
+ assert @rw_handler.retrieve_connection(@spec_name)
+ assert @ro_handler.retrieve_connection(@spec_name)
+ end
+
+ def test_active_connections?
+ assert_not_predicate @rw_handler, :active_connections?
+ assert_not_predicate @ro_handler, :active_connections?
+
+ assert @rw_handler.retrieve_connection(@spec_name)
+ assert @ro_handler.retrieve_connection(@spec_name)
+
+ assert_predicate @rw_handler, :active_connections?
+ assert_predicate @ro_handler, :active_connections?
+
+ @rw_handler.clear_active_connections!
+ assert_not_predicate @rw_handler, :active_connections?
+
+ @ro_handler.clear_active_connections!
+ assert_not_predicate @ro_handler, :active_connections?
+ end
+
+ def test_retrieve_connection_pool
+ assert_not_nil @rw_handler.retrieve_connection_pool(@spec_name)
+ assert_not_nil @ro_handler.retrieve_connection_pool(@spec_name)
+ end
+
+ def test_retrieve_connection_pool_with_invalid_id
+ 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
+
+ def test_calling_connected_to_on_a_non_existent_handler_raises
+ error = assert_raises ActiveRecord::ConnectionNotEstablished do
+ ActiveRecord::Base.connected_to(role: :reading) do
+ Person.first
+ end
+ end
+
+ assert_equal "No connection pool with 'primary' found for the 'reading' role.", error.message
+ end
+
+ def test_default_handlers_are_writing_and_reading
+ assert_equal :writing, ActiveRecord::Base.writing_role
+ assert_equal :reading, ActiveRecord::Base.reading_role
+ end
+
+ def test_an_application_can_change_the_default_handlers
+ old_writing = ActiveRecord::Base.writing_role
+ old_reading = ActiveRecord::Base.reading_role
+ ActiveRecord::Base.writing_role = :default
+ ActiveRecord::Base.reading_role = :readonly
+
+ assert_equal :default, ActiveRecord::Base.writing_role
+ assert_equal :readonly, ActiveRecord::Base.reading_role
+ ensure
+ ActiveRecord::Base.writing_role = old_writing
+ ActiveRecord::Base.reading_role = old_reading
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
index 1b64324cc4..515bf5df06 100644
--- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
+++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb
@@ -18,11 +18,14 @@ module ActiveRecord
end
def resolve_config(config)
- ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve
+ configs = ActiveRecord::DatabaseConfigurations.new(config)
+ configs.to_h
end
def resolve_spec(spec, config)
- ConnectionSpecification::Resolver.new(resolve_config(config)).resolve(spec)
+ configs = ActiveRecord::DatabaseConfigurations.new(config)
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs)
+ resolver.resolve(spec, spec)
end
def test_resolver_with_database_uri_and_current_env_symbol_key
@@ -43,6 +46,14 @@ module ActiveRecord
assert_equal expected, actual
end
+ def test_resolver_with_nil_database_url_and_current_env
+ ENV["RAILS_ENV"] = "foo"
+ config = { "foo" => { "adapter" => "postgres", "url" => ENV["DATABASE_URL"] } }
+ actual = resolve_spec(:foo, config)
+ expected = { "adapter" => "postgres", "url" => nil, "name" => "foo" }
+ assert_equal expected, actual
+ end
+
def test_resolver_with_database_uri_and_current_env_symbol_key_and_rack_env
ENV["DATABASE_URL"] = "postgres://localhost/foo"
ENV["RACK_ENV"] = "foo"
@@ -61,6 +72,16 @@ module ActiveRecord
assert_equal expected, actual
end
+ def test_resolver_with_database_uri_and_multiple_envs
+ ENV["DATABASE_URL"] = "postgres://localhost"
+ ENV["RAILS_ENV"] = "test"
+
+ config = { "production" => { "adapter" => "postgresql", "database" => "foo_prod" }, "test" => { "adapter" => "postgresql", "database" => "foo_test" } }
+ actual = resolve_spec(:test, config)
+ expected = { "adapter" => "postgresql", "database" => "foo_test", "host" => "localhost", "name" => "test" }
+ assert_equal expected, actual
+ end
+
def test_resolver_with_database_uri_and_unknown_symbol_key
ENV["DATABASE_URL"] = "postgres://localhost/foo"
config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } }
diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
index 02e76ce146..38331aa641 100644
--- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
+++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb
@@ -27,8 +27,12 @@ if current_adapter?(:Mysql2Adapter)
def test_string_types
assert_lookup_type :string, "enum('one', 'two', 'three')"
assert_lookup_type :string, "ENUM('one', 'two', 'three')"
+ assert_lookup_type :string, "enum ('one', 'two', 'three')"
+ assert_lookup_type :string, "ENUM ('one', 'two', 'three')"
assert_lookup_type :string, "set('one', 'two', 'three')"
assert_lookup_type :string, "SET('one', 'two', 'three')"
+ assert_lookup_type :string, "set ('one', 'two', 'three')"
+ assert_lookup_type :string, "SET ('one', 'two', 'three')"
end
def test_set_type_with_value_matching_other_type
diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
index 67496381d1..89a9c30f9b 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -6,8 +6,9 @@ module ActiveRecord
module ConnectionAdapters
class SchemaCacheTest < ActiveRecord::TestCase
def setup
- connection = ActiveRecord::Base.connection
- @cache = SchemaCache.new connection
+ @connection = ActiveRecord::Base.connection
+ @cache = SchemaCache.new @connection
+ @database_version = @connection.get_database_version
end
def test_primary_key
@@ -19,6 +20,7 @@ module ActiveRecord
@cache.columns_hash("posts")
@cache.data_sources("posts")
@cache.primary_keys("posts")
+ @cache.indexes("posts")
new_cache = YAML.load(YAML.dump(@cache))
assert_no_queries do
@@ -26,19 +28,47 @@ module ActiveRecord
assert_equal 12, new_cache.columns_hash("posts").size
assert new_cache.data_sources("posts")
assert_equal "id", new_cache.primary_keys("posts")
+ assert_equal 1, new_cache.indexes("posts").size
+ assert_equal @database_version.to_s, new_cache.database_version.to_s
end
end
def test_yaml_loads_5_1_dump
- body = File.open(schema_dump_path).read
- cache = YAML.load(body)
+ @cache = YAML.load(File.read(schema_dump_path))
assert_no_queries do
- assert_equal 11, cache.columns("posts").size
- assert_equal 11, cache.columns_hash("posts").size
- assert cache.data_sources("posts")
- assert_equal "id", cache.primary_keys("posts")
+ assert_equal 11, @cache.columns("posts").size
+ assert_equal 11, @cache.columns_hash("posts").size
+ assert @cache.data_sources("posts")
+ assert_equal "id", @cache.primary_keys("posts")
+ end
+ end
+
+ def test_yaml_loads_5_1_dump_without_indexes_still_queries_for_indexes
+ @cache = YAML.load(File.read(schema_dump_path))
+
+ # Simulate assignment in railtie after loading the cache.
+ old_cache, @connection.schema_cache = @connection.schema_cache, @cache
+
+ assert_queries :any, ignore_none: true do
+ assert_equal 1, @cache.indexes("posts").size
end
+ ensure
+ @connection.schema_cache = old_cache
+ end
+
+ def test_yaml_loads_5_1_dump_without_database_version_still_queries_for_database_version
+ @cache = YAML.load(File.read(schema_dump_path))
+
+ # Simulate assignment in railtie after loading the cache.
+ old_cache, @connection.schema_cache = @connection.schema_cache, @cache
+
+ # We can't verify queries get executed because the database version gets
+ # cached in both MySQL and PostgreSQL outside of the schema cache.
+ assert_nil @cache.instance_variable_get(:@database_version)
+ assert_equal @database_version.to_s, @cache.database_version.to_s
+ ensure
+ @connection.schema_cache = old_cache
end
def test_primary_key_for_non_existent_table
@@ -55,15 +85,30 @@ module ActiveRecord
assert_equal columns_hash, @cache.columns_hash("posts")
end
+ def test_caches_indexes
+ indexes = @cache.indexes("posts")
+ assert_equal indexes, @cache.indexes("posts")
+ end
+
+ def test_caches_database_version
+ @cache.database_version # cache database_version
+
+ assert_no_queries do
+ assert_equal @database_version.to_s, @cache.database_version.to_s
+ end
+ end
+
def test_clearing
@cache.columns("posts")
@cache.columns_hash("posts")
@cache.data_sources("posts")
@cache.primary_keys("posts")
+ @cache.indexes("posts")
@cache.clear!
assert_equal 0, @cache.size
+ assert_nil @cache.instance_variable_get(:@database_version)
end
def test_dump_and_load
@@ -71,6 +116,7 @@ module ActiveRecord
@cache.columns_hash("posts")
@cache.data_sources("posts")
@cache.primary_keys("posts")
+ @cache.indexes("posts")
@cache = Marshal.load(Marshal.dump(@cache))
@@ -79,6 +125,8 @@ module ActiveRecord
assert_equal 12, @cache.columns_hash("posts").size
assert @cache.data_sources("posts")
assert_equal "id", @cache.primary_keys("posts")
+ assert_equal 1, @cache.indexes("posts").size
+ assert_equal @database_version.to_s, @cache.database_version.to_s
end
end
@@ -91,8 +139,23 @@ module ActiveRecord
@cache.clear_data_source_cache!("posts")
end
- private
+ test "#columns_hash? is populated by #columns_hash" do
+ assert_not @cache.columns_hash?("posts")
+
+ @cache.columns_hash("posts")
+ assert @cache.columns_hash?("posts")
+ end
+
+ test "#columns_hash? is not populated by #data_source_exists?" do
+ assert_not @cache.columns_hash?("posts")
+
+ @cache.data_source_exists?("posts")
+
+ assert_not @cache.columns_hash?("posts")
+ end
+
+ private
def schema_dump_path
"test/assets/schema_dump_5_1.yml"
end
diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb
index 0941ee3309..b9b5cc0e28 100644
--- a/activerecord/test/cases/connection_management_test.rb
+++ b/activerecord/test/cases/connection_management_test.rb
@@ -106,7 +106,7 @@ module ActiveRecord
def middleware(app)
lambda do |env|
a, b, c = executor.wrap { app.call(env) }
- [a, b, Rack::BodyProxy.new(c) {}]
+ [a, b, Rack::BodyProxy.new(c) { }]
end
end
end
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
index 9ac03629c3..a15ad9a45b 100644
--- a/activerecord/test/cases/connection_pool_test.rb
+++ b/activerecord/test/cases/connection_pool_test.rb
@@ -91,7 +91,9 @@ module ActiveRecord
end
def test_full_pool_exception
+ @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout
@pool.size.times { assert @pool.checkout }
+
assert_raises(ConnectionTimeoutError) do
@pool.checkout
end
@@ -109,6 +111,44 @@ module ActiveRecord
assert_equal connection, t.join.value
end
+ def test_full_pool_blocking_shares_load_interlock
+ @pool.instance_variable_set(:@size, 1)
+
+ load_interlock_latch = Concurrent::CountDownLatch.new
+ connection_latch = Concurrent::CountDownLatch.new
+
+ able_to_get_connection = false
+ able_to_load = false
+
+ thread_with_load_interlock = Thread.new do
+ ActiveSupport::Dependencies.interlock.running do
+ load_interlock_latch.count_down
+ connection_latch.wait
+
+ @pool.with_connection do
+ able_to_get_connection = true
+ end
+ end
+ end
+
+ thread_with_last_connection = Thread.new do
+ @pool.with_connection do
+ connection_latch.count_down
+ load_interlock_latch.wait
+
+ ActiveSupport::Dependencies.interlock.loading do
+ able_to_load = true
+ end
+ end
+ end
+
+ thread_with_load_interlock.join
+ thread_with_last_connection.join
+
+ assert able_to_get_connection
+ assert able_to_load
+ end
+
def test_removing_releases_latch
cs = @pool.size.times.map { @pool.checkout }
t = Thread.new { @pool.checkout }
@@ -156,6 +196,48 @@ module ActiveRecord
@pool.connections.each { |conn| conn.close if conn.in_use? }
end
+ def test_idle_timeout_configuration
+ @pool.disconnect!
+ spec = ActiveRecord::Base.connection_pool.spec
+ spec.config.merge!(idle_timeout: "0.02")
+ @pool = ConnectionPool.new(spec)
+ idle_conn = @pool.checkout
+ @pool.checkin(idle_conn)
+
+ idle_conn.instance_variable_set(
+ :@idle_since,
+ Concurrent.monotonic_time - 0.01
+ )
+
+ @pool.flush
+ assert_equal 1, @pool.connections.length
+
+ idle_conn.instance_variable_set(
+ :@idle_since,
+ Concurrent.monotonic_time - 0.02
+ )
+
+ @pool.flush
+ assert_equal 0, @pool.connections.length
+ end
+
+ def test_disable_flush
+ @pool.disconnect!
+ spec = ActiveRecord::Base.connection_pool.spec
+ spec.config.merge!(idle_timeout: -5)
+ @pool = ConnectionPool.new(spec)
+ idle_conn = @pool.checkout
+ @pool.checkin(idle_conn)
+
+ idle_conn.instance_variable_set(
+ :@idle_since,
+ Concurrent.monotonic_time - 1
+ )
+
+ @pool.flush
+ assert_equal 1, @pool.connections.length
+ end
+
def test_flush
idle_conn = @pool.checkout
recent_conn = @pool.checkout
@@ -166,9 +248,10 @@ module ActiveRecord
assert_equal 3, @pool.connections.length
- def idle_conn.seconds_idle
- 1000
- end
+ idle_conn.instance_variable_set(
+ :@idle_since,
+ Concurrent.monotonic_time - 1000
+ )
@pool.flush(30)
@@ -484,23 +567,21 @@ module ActiveRecord
def test_disconnect_and_clear_reloadable_connections_attempt_to_wait_for_threads_to_return_their_conns
[:disconnect, :disconnect!, :clear_reloadable_connections, :clear_reloadable_connections!].each do |group_action_method|
- begin
- thread = timed_join_result = nil
- @pool.with_connection do |connection|
- thread = Thread.new { @pool.send(group_action_method) }
-
- # give the other `thread` some time to get stuck in `group_action_method`
- timed_join_result = thread.join(0.3)
- # thread.join # => `nil` means the other thread hasn't finished running and is still waiting for us to
- # release our connection
- assert_nil timed_join_result
-
- # assert that since this is within default timeout our connection hasn't been forcefully taken away from us
- assert_predicate @pool, :active_connection?
- end
- ensure
- thread.join if thread && !timed_join_result # clean up the other thread
+ thread = timed_join_result = nil
+ @pool.with_connection do |connection|
+ thread = Thread.new { @pool.send(group_action_method) }
+
+ # give the other `thread` some time to get stuck in `group_action_method`
+ timed_join_result = thread.join(0.3)
+ # thread.join # => `nil` means the other thread hasn't finished running and is still waiting for us to
+ # release our connection
+ assert_nil timed_join_result
+
+ # assert that since this is within default timeout our connection hasn't been forcefully taken away from us
+ assert_predicate @pool, :active_connection?
end
+ ensure
+ thread.join if thread && !timed_join_result # clean up the other thread
end
end
@@ -578,7 +659,7 @@ module ActiveRecord
end
stuck_thread = Thread.new do
- pool.with_connection {}
+ pool.with_connection { }
end
# wait for stuck_thread to get in queue
diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb
index 5b80f16a44..72be14f507 100644
--- a/activerecord/test/cases/connection_specification/resolver_test.rb
+++ b/activerecord/test/cases/connection_specification/resolver_test.rb
@@ -7,11 +7,15 @@ module ActiveRecord
class ConnectionSpecification
class ResolverTest < ActiveRecord::TestCase
def resolve(spec, config = {})
- Resolver.new(config).resolve(spec)
+ configs = ActiveRecord::DatabaseConfigurations.new(config)
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs)
+ resolver.resolve(spec, spec)
end
def spec(spec, config = {})
- Resolver.new(config).spec(spec)
+ configs = ActiveRecord::DatabaseConfigurations.new(config)
+ resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs)
+ resolver.spec(spec)
end
def test_url_invalid_adapter
diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb
index 6e7ae2efb4..36e3d543cd 100644
--- a/activerecord/test/cases/core_test.rb
+++ b/activerecord/test/cases/core_test.rb
@@ -30,13 +30,18 @@ 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
def test_pretty_print_new
topic = Topic.new
- actual = "".dup
+ actual = +""
PP.pp(topic, StringIO.new(actual))
expected = <<~PRETTY
#<Topic:0xXXXXXX
@@ -65,7 +70,7 @@ class CoreTest < ActiveRecord::TestCase
def test_pretty_print_persisted
topic = topics(:first)
- actual = "".dup
+ actual = +""
PP.pp(topic, StringIO.new(actual))
expected = <<~PRETTY
#<Topic:0x\\w+
@@ -93,7 +98,7 @@ class CoreTest < ActiveRecord::TestCase
def test_pretty_print_uninitialized
topic = Topic.allocate
- actual = "".dup
+ actual = +""
PP.pp(topic, StringIO.new(actual))
expected = "#<Topic:XXXXXX not initialized>\n"
assert actual.start_with?(expected.split("XXXXXX").first)
@@ -106,8 +111,15 @@ class CoreTest < ActiveRecord::TestCase
"inspecting topic"
end
end
- actual = "".dup
+ actual = +""
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/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb
index e0948f90ac..cc4f86a0fb 100644
--- a/activerecord/test/cases/counter_cache_test.rb
+++ b/activerecord/test/cases/counter_cache_test.rb
@@ -144,7 +144,7 @@ class CounterCacheTest < ActiveRecord::TestCase
test "update other counters on parent destroy" do
david, joanna = dog_lovers(:david, :joanna)
- joanna = joanna # squelch a warning
+ _ = joanna # squelch a warning
assert_difference "joanna.reload.dogs_count", -1 do
david.destroy
@@ -280,38 +280,38 @@ class CounterCacheTest < ActiveRecord::TestCase
end
test "update counters with touch: :written_on" do
- assert_touching @topic, :written_on do
+ assert_touching @topic, :updated_at, :written_on do
Topic.update_counters(@topic.id, replies_count: -1, touch: :written_on)
end
end
test "update multiple counters with touch: :written_on" do
- assert_touching @topic, :written_on do
+ assert_touching @topic, :updated_at, :written_on do
Topic.update_counters(@topic.id, replies_count: 2, unique_replies_count: 2, touch: :written_on)
end
end
test "reset counters with touch: :written_on" do
- assert_touching @topic, :written_on do
+ assert_touching @topic, :updated_at, :written_on do
Topic.reset_counters(@topic.id, :replies, touch: :written_on)
end
end
test "reset multiple counters with touch: :written_on" do
- assert_touching @topic, :written_on do
+ assert_touching @topic, :updated_at, :written_on do
Topic.update_counters(@topic.id, replies_count: 1, unique_replies_count: 1)
Topic.reset_counters(@topic.id, :replies, :unique_replies, touch: :written_on)
end
end
test "increment counters with touch: :written_on" do
- assert_touching @topic, :written_on do
+ assert_touching @topic, :updated_at, :written_on do
Topic.increment_counter(:replies_count, @topic.id, touch: :written_on)
end
end
test "decrement counters with touch: :written_on" do
- assert_touching @topic, :written_on do
+ assert_touching @topic, :updated_at, :written_on do
Topic.decrement_counter(:replies_count, @topic.id, touch: :written_on)
end
end
diff --git a/activerecord/test/cases/database_configurations_test.rb b/activerecord/test/cases/database_configurations_test.rb
new file mode 100644
index 0000000000..ed8151f01a
--- /dev/null
+++ b/activerecord/test/cases/database_configurations_test.rb
@@ -0,0 +1,117 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+
+class DatabaseConfigurationsTest < ActiveRecord::TestCase
+ unless in_memory_db?
+ def test_empty_returns_true_when_db_configs_are_empty
+ old_config = ActiveRecord::Base.configurations
+ config = {}
+
+ ActiveRecord::Base.configurations = config
+
+ assert_predicate ActiveRecord::Base.configurations, :empty?
+ assert_predicate ActiveRecord::Base.configurations, :blank?
+ ensure
+ ActiveRecord::Base.configurations = old_config
+ ActiveRecord::Base.establish_connection :arunit
+ end
+ end
+
+ def test_configs_for_getter_with_env_name
+ configs = ActiveRecord::Base.configurations.configs_for(env_name: "arunit")
+
+ assert_equal 1, configs.size
+ assert_equal ["arunit"], configs.map(&:env_name)
+ end
+
+ def test_configs_for_getter_with_env_and_spec_name
+ config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", spec_name: "primary")
+
+ assert_equal "arunit", config.env_name
+ assert_equal "primary", config.spec_name
+ end
+
+ def test_default_hash_returns_config_hash_from_default_env
+ original_rails_env = ENV["RAILS_ENV"]
+ ENV["RAILS_ENV"] = "arunit"
+
+ assert_equal ActiveRecord::Base.configurations.configs_for(env_name: "arunit", spec_name: "primary").config, ActiveRecord::Base.configurations.default_hash
+ ensure
+ ENV["RAILS_ENV"] = original_rails_env
+ end
+
+ def test_find_db_config_returns_a_db_config_object_for_the_given_env
+ config = ActiveRecord::Base.configurations.find_db_config("arunit2")
+
+ assert_equal "arunit2", config.env_name
+ assert_equal "primary", config.spec_name
+ end
+
+ def test_to_h_turns_db_config_object_back_into_a_hash
+ configs = ActiveRecord::Base.configurations
+ assert_equal "ActiveRecord::DatabaseConfigurations", configs.class.name
+ assert_equal "Hash", configs.to_h.class.name
+ assert_equal ["arunit", "arunit2", "arunit_without_prepared_statements"], ActiveRecord::Base.configurations.to_h.keys.sort
+ end
+end
+
+class LegacyDatabaseConfigurationsTest < ActiveRecord::TestCase
+ unless in_memory_db?
+ def test_setting_configurations_hash
+ old_config = ActiveRecord::Base.configurations
+ config = { "adapter" => "sqlite3" }
+
+ assert_deprecated do
+ ActiveRecord::Base.configurations["readonly"] = config
+ end
+
+ assert_equal ["arunit", "arunit2", "arunit_without_prepared_statements", "readonly"], ActiveRecord::Base.configurations.configs_for.map(&:env_name).sort
+ ensure
+ ActiveRecord::Base.configurations = old_config
+ ActiveRecord::Base.establish_connection :arunit
+ end
+ end
+
+ def test_can_turn_configurations_into_a_hash
+ assert ActiveRecord::Base.configurations.to_h.is_a?(Hash), "expected to be a hash but was not."
+ assert_equal ["arunit", "arunit2", "arunit_without_prepared_statements"].sort, ActiveRecord::Base.configurations.to_h.keys.sort
+ end
+
+ def test_each_is_deprecated
+ assert_deprecated do
+ ActiveRecord::Base.configurations.each do |db_config|
+ assert_equal "primary", db_config.spec_name
+ end
+ end
+ end
+
+ def test_first_is_deprecated
+ assert_deprecated do
+ db_config = ActiveRecord::Base.configurations.first
+ assert_equal "arunit", db_config.env_name
+ assert_equal "primary", db_config.spec_name
+ end
+ end
+
+ def test_fetch_is_deprecated
+ assert_deprecated do
+ db_config = ActiveRecord::Base.configurations.fetch("arunit").first
+ assert_equal "arunit", db_config.env_name
+ assert_equal "primary", db_config.spec_name
+ end
+ end
+
+ def test_values_are_deprecated
+ config_hashes = ActiveRecord::Base.configurations.configurations.map(&:config)
+ assert_deprecated do
+ assert_equal config_hashes, ActiveRecord::Base.configurations.values
+ end
+ end
+
+ def test_unsupported_method_raises
+ assert_raises NotImplementedError do
+ ActiveRecord::Base.configurations.select { |a| a == "foo" }
+ end
+ end
+end
diff --git a/activerecord/test/cases/database_selector_test.rb b/activerecord/test/cases/database_selector_test.rb
new file mode 100644
index 0000000000..fd02d2acb4
--- /dev/null
+++ b/activerecord/test/cases/database_selector_test.rb
@@ -0,0 +1,166 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/person"
+require "action_dispatch"
+
+module ActiveRecord
+ class DatabaseSelectorTest < ActiveRecord::TestCase
+ setup do
+ @session_store = {}
+ @session = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.new(@session_store)
+ end
+
+ teardown do
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
+ end
+
+ def test_empty_session
+ assert_equal Time.at(0), @session.last_write_timestamp
+ end
+
+ def test_writing_the_session_timestamps
+ assert @session.update_last_write_timestamp
+
+ session2 = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.new(@session_store)
+ assert_equal @session.last_write_timestamp, session2.last_write_timestamp
+ end
+
+ def test_writing_session_time_changes
+ assert @session.update_last_write_timestamp
+
+ before = @session.last_write_timestamp
+ sleep(0.1)
+
+ assert @session.update_last_write_timestamp
+ assert_not_equal before, @session.last_write_timestamp
+ end
+
+ def test_read_from_replicas
+ @session_store[:last_write] = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.convert_time_to_timestamp(Time.now - 5.seconds)
+
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)
+
+ called = false
+ resolver.read do
+ called = true
+ assert ActiveRecord::Base.connected_to?(role: :reading)
+ end
+ assert called
+ end
+
+ def test_read_from_primary
+ @session_store[:last_write] = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session.convert_time_to_timestamp(Time.now)
+
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)
+
+ called = false
+ resolver.read do
+ called = true
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ end
+ assert called
+ end
+
+ def test_write_to_primary
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)
+
+ # Session should start empty
+ assert_nil @session_store[:last_write]
+
+ called = false
+ resolver.write do
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ called = true
+ end
+ assert called
+
+ # and be populated by the last write time
+ assert @session_store[:last_write]
+ end
+
+ def test_write_to_primary_with_exception
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session)
+
+ # Session should start empty
+ assert_nil @session_store[:last_write]
+
+ called = false
+ assert_raises(ActiveRecord::RecordNotFound) do
+ resolver.write do
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ called = true
+ raise ActiveRecord::RecordNotFound
+ end
+ end
+ assert called
+
+ # and be populated by the last write time
+ assert @session_store[:last_write]
+ end
+
+ def test_read_from_primary_with_options
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 5.seconds)
+
+ # Session should start empty
+ assert_nil @session_store[:last_write]
+
+ called = false
+ resolver.write do
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ called = true
+ end
+ assert called
+
+ # and be populated by the last write time
+ assert @session_store[:last_write]
+
+ read = false
+ resolver.read do
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ read = true
+ end
+ assert read
+ end
+
+ def test_read_from_replica_with_no_delay
+ resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver.new(@session, delay: 0.seconds)
+
+ # Session should start empty
+ assert_nil @session_store[:last_write]
+
+ called = false
+ resolver.write do
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ called = true
+ end
+ assert called
+
+ # and be populated by the last write time
+ assert @session_store[:last_write]
+
+ read = false
+ resolver.read do
+ assert ActiveRecord::Base.connected_to?(role: :reading)
+ read = true
+ end
+ assert read
+ end
+
+ def test_the_middleware_chooses_writing_role_with_POST_request
+ middleware = ActiveRecord::Middleware::DatabaseSelector.new(lambda { |env|
+ assert ActiveRecord::Base.connected_to?(role: :writing)
+ [200, {}, ["body"]]
+ })
+ assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "POST")
+ end
+
+ def test_the_middleware_chooses_reading_role_with_GET_request
+ middleware = ActiveRecord::Middleware::DatabaseSelector.new(lambda { |env|
+ assert ActiveRecord::Base.connected_to?(role: :reading)
+ [200, {}, ["body"]]
+ })
+ assert_equal [200, {}, ["body"]], middleware.call("REQUEST_METHOD" => "GET")
+ end
+ end
+end
diff --git a/activerecord/test/cases/date_test.rb b/activerecord/test/cases/date_test.rb
index 9f412cdb63..2475d4a34b 100644
--- a/activerecord/test/cases/date_test.rb
+++ b/activerecord/test/cases/date_test.rb
@@ -23,23 +23,13 @@ class DateTest < ActiveRecord::TestCase
valid_dates.each do |date_src|
topic = Topic.new("last_read(1i)" => date_src[0].to_s, "last_read(2i)" => date_src[1].to_s, "last_read(3i)" => date_src[2].to_s)
- # Oracle DATE columns are datetime columns and Oracle adapter returns Time value
- if current_adapter?(:OracleAdapter)
- assert_equal(topic.last_read.to_date, Date.new(*date_src))
- else
- assert_equal(topic.last_read, Date.new(*date_src))
- end
+ assert_equal(topic.last_read, Date.new(*date_src))
end
invalid_dates.each do |date_src|
assert_nothing_raised do
topic = Topic.new("last_read(1i)" => date_src[0].to_s, "last_read(2i)" => date_src[1].to_s, "last_read(3i)" => date_src[2].to_s)
- # Oracle DATE columns are datetime columns and Oracle adapter returns Time value
- if current_adapter?(:OracleAdapter)
- assert_equal(topic.last_read.to_date, Time.local(*date_src).to_date, "The date should be modified according to the behavior of the Time object")
- else
- assert_equal(topic.last_read, Time.local(*date_src).to_date, "The date should be modified according to the behavior of the Time object")
- end
+ assert_equal(topic.last_read, Time.local(*date_src).to_date, "The date should be modified according to the behavior of the Time object")
end
end
end
diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb
index e64a8372d0..79d63949ca 100644
--- a/activerecord/test/cases/date_time_precision_test.rb
+++ b/activerecord/test/cases/date_time_precision_test.rb
@@ -29,7 +29,7 @@ if subsecond_precision_supported?
def test_datetime_precision_is_truncated_on_assignment
@connection.create_table(:foos, force: true)
- @connection.add_column :foos, :created_at, :datetime, precision: 0
+ @connection.add_column :foos, :created_at, :datetime, precision: 0
@connection.add_column :foos, :updated_at, :datetime, precision: 6
time = ::Time.now.change(nsec: 123456789)
@@ -45,6 +45,26 @@ if subsecond_precision_supported?
assert_equal 123456000, foo.updated_at.nsec
end
+ unless current_adapter?(:Mysql2Adapter)
+ def test_no_datetime_precision_isnt_truncated_on_assignment
+ @connection.create_table(:foos, force: true)
+ @connection.add_column :foos, :created_at, :datetime
+ @connection.add_column :foos, :updated_at, :datetime, precision: 6
+
+ time = ::Time.now.change(nsec: 123)
+ foo = Foo.new(created_at: time, updated_at: time)
+
+ assert_equal 123, foo.created_at.nsec
+ assert_equal 0, foo.updated_at.nsec
+
+ foo.save!
+ foo.reload
+
+ assert_equal 0, foo.created_at.nsec
+ assert_equal 0, foo.updated_at.nsec
+ end
+ end
+
def test_timestamps_helper_with_custom_precision
@connection.create_table(:foos, force: true) do |t|
t.timestamps precision: 4
@@ -62,7 +82,7 @@ if subsecond_precision_supported?
end
def test_invalid_datetime_precision_raises_error
- assert_raises ActiveRecord::ActiveRecordError do
+ assert_raises ArgumentError do
@connection.create_table(:foos, force: true) do |t|
t.timestamps precision: 7
end
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index 3d11b573f1..50a86b0a19 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -9,7 +9,7 @@ class DefaultTest < ActiveRecord::TestCase
def test_nil_defaults_for_not_null_columns
%w(id name course_id).each do |name|
column = Entrant.columns_hash[name]
- assert !column.null, "#{name} column should be NOT NULL"
+ assert_not column.null, "#{name} column should be NOT NULL"
assert_not column.default, "#{name} column should be DEFAULT 'nil'"
end
end
@@ -89,7 +89,7 @@ if current_adapter?(:PostgreSQLAdapter)
test "schema dump includes default expression" do
output = dump_table_schema("defaults")
- if ActiveRecord::Base.connection.postgresql_version >= 100000
+ if ActiveRecord::Base.connection.database_version >= 100000
assert_match %r/t\.date\s+"modified_date",\s+default: -> { "CURRENT_DATE" }/, output
assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "CURRENT_TIMESTAMP" }/, output
else
@@ -106,21 +106,38 @@ if current_adapter?(:Mysql2Adapter)
class MysqlDefaultExpressionTest < ActiveRecord::TestCase
include SchemaDumpingHelper
- if ActiveRecord::Base.connection.version >= "5.6.0"
+ 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")
- assert_match %r/t\.datetime\s+"modified_datetime",\s+default: -> { "CURRENT_TIMESTAMP" }/, output
+ assert_match %r/t\.datetime\s+"modified_datetime",\s+default: -> { "CURRENT_TIMESTAMP(?:\(\))?" }/i, output
end
- end
- test "schema dump timestamp includes default expression" do
- output = dump_table_schema("timestamp_defaults")
- assert_match %r/t\.timestamp\s+"modified_timestamp",\s+default: -> { "CURRENT_TIMESTAMP" }/, output
- end
+ test "schema dump datetime includes precise default expression" do
+ output = dump_table_schema("datetime_defaults")
+ assert_match %r/t\.datetime\s+"precise_datetime",.+default: -> { "CURRENT_TIMESTAMP\(6\)" }/i, output
+ end
+
+ test "schema dump timestamp includes default expression" do
+ output = dump_table_schema("timestamp_defaults")
+ assert_match %r/t\.timestamp\s+"modified_timestamp",\s+default: -> { "CURRENT_TIMESTAMP(?:\(\))?" }/i, output
+ end
- test "schema dump timestamp without default expression" do
- output = dump_table_schema("timestamp_defaults")
- assert_match %r/t\.timestamp\s+"nullable_timestamp"$/, output
+ test "schema dump timestamp includes precise default expression" do
+ output = dump_table_schema("timestamp_defaults")
+ assert_match %r/t\.timestamp\s+"precise_timestamp",.+default: -> { "CURRENT_TIMESTAMP\(6\)" }/i, output
+ end
+
+ test "schema dump timestamp without default expression" do
+ output = dump_table_schema("timestamp_defaults")
+ assert_match %r/t\.timestamp\s+"nullable_timestamp"$/, output
+ end
end
end
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index 5c7f70b7a0..a2a501a794 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -336,7 +336,7 @@ class DirtyTest < ActiveRecord::TestCase
end
with_partial_writes Pirate, true do
- assert_queries(0) { 2.times { pirate.save! } }
+ assert_no_queries { 2.times { pirate.save! } }
assert_equal old_updated_on, pirate.reload.updated_on
assert_queries(1) { pirate.catchphrase = "bar"; pirate.save! }
@@ -352,10 +352,10 @@ class DirtyTest < ActiveRecord::TestCase
Person.where(id: person.id).update_all(first_name: "baz")
end
- old_lock_version = person.lock_version
+ old_lock_version = person.lock_version + 1
with_partial_writes Person, true do
- assert_queries(0) { 2.times { person.save! } }
+ assert_no_queries { 2.times { person.save! } }
assert_equal old_lock_version, person.reload.lock_version
assert_queries(1) { person.first_name = "bar"; person.save! }
@@ -366,7 +366,7 @@ class DirtyTest < ActiveRecord::TestCase
def test_changed_attributes_should_be_preserved_if_save_failure
pirate = Pirate.new
pirate.parrot_id = 1
- assert !pirate.save
+ assert_not pirate.save
check_pirate_after_save_failure(pirate)
pirate = Pirate.new
@@ -496,7 +496,7 @@ class DirtyTest < ActiveRecord::TestCase
assert_not_nil pirate.previous_changes["updated_on"][1]
assert_nil pirate.previous_changes["created_on"][0]
assert_not_nil pirate.previous_changes["created_on"][1]
- assert !pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("parrot_id")
# original values should be in previous_changes
pirate = Pirate.new
@@ -510,7 +510,7 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal [nil, pirate.id], pirate.previous_changes["id"]
assert_includes pirate.previous_changes, "updated_on"
assert_includes pirate.previous_changes, "created_on"
- assert !pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("parrot_id")
pirate.catchphrase = "Yar!!"
pirate.reload
@@ -527,8 +527,8 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal ["arrr", "Me Maties!"], pirate.previous_changes["catchphrase"]
assert_not_nil pirate.previous_changes["updated_on"][0]
assert_not_nil pirate.previous_changes["updated_on"][1]
- assert !pirate.previous_changes.key?("parrot_id")
- assert !pirate.previous_changes.key?("created_on")
+ assert_not pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("created_on")
pirate = Pirate.find_by_catchphrase("Me Maties!")
@@ -541,8 +541,8 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal ["Me Maties!", "Thar She Blows!"], pirate.previous_changes["catchphrase"]
assert_not_nil pirate.previous_changes["updated_on"][0]
assert_not_nil pirate.previous_changes["updated_on"][1]
- assert !pirate.previous_changes.key?("parrot_id")
- assert !pirate.previous_changes.key?("created_on")
+ assert_not pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("created_on")
travel(1.second)
@@ -553,8 +553,8 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal ["Thar She Blows!", "Ahoy!"], pirate.previous_changes["catchphrase"]
assert_not_nil pirate.previous_changes["updated_on"][0]
assert_not_nil pirate.previous_changes["updated_on"][1]
- assert !pirate.previous_changes.key?("parrot_id")
- assert !pirate.previous_changes.key?("created_on")
+ assert_not pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("created_on")
travel(1.second)
@@ -565,10 +565,8 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal ["Ahoy!", "Ninjas suck!"], pirate.previous_changes["catchphrase"]
assert_not_nil pirate.previous_changes["updated_on"][0]
assert_not_nil pirate.previous_changes["updated_on"][1]
- assert !pirate.previous_changes.key?("parrot_id")
- assert !pirate.previous_changes.key?("created_on")
- ensure
- travel_back
+ assert_not pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("created_on")
end
class Testings < ActiveRecord::Base; end
@@ -879,6 +877,26 @@ class DirtyTest < ActiveRecord::TestCase
raise "changed? should be false" if changed?
raise "has_changes_to_save? should be false" if has_changes_to_save?
raise "saved_changes? should be true" unless saved_changes?
+ raise "id_in_database should not be nil" if id_in_database.nil?
+ end
+ end
+
+ person = klass.create!(first_name: "Sean")
+ assert_not_predicate person, :changed?
+ end
+
+ test "changed? in around callbacks after yield returns false" do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "people"
+
+ around_create :check_around
+
+ def check_around
+ yield
+ raise "changed? should be false" if changed?
+ raise "has_changes_to_save? should be false" if has_changes_to_save?
+ raise "saved_changes? should be true" unless saved_changes?
+ raise "id_in_database should not be nil" if id_in_database.nil?
end
end
diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb
index 9e33c3110c..a2efbf89f9 100644
--- a/activerecord/test/cases/dup_test.rb
+++ b/activerecord/test/cases/dup_test.rb
@@ -17,7 +17,7 @@ module ActiveRecord
topic = Topic.first
duped = topic.dup
- assert !duped.readonly?, "should not be readonly"
+ assert_not duped.readonly?, "should not be readonly"
end
def test_is_readonly
@@ -32,7 +32,7 @@ module ActiveRecord
topic = Topic.first
duped = topic.dup
- assert !duped.persisted?, "topic not persisted"
+ assert_not duped.persisted?, "topic not persisted"
assert duped.new_record?, "topic is new"
end
@@ -140,7 +140,7 @@ module ActiveRecord
prev_default_scopes = Topic.default_scopes
Topic.default_scopes = [proc { Topic.where(approved: true) }]
topic = Topic.new(approved: false)
- assert !topic.dup.approved?, "should not be overridden by default scopes"
+ assert_not topic.dup.approved?, "should not be overridden by default scopes"
ensure
Topic.default_scopes = prev_default_scopes
end
@@ -171,7 +171,7 @@ module ActiveRecord
end
end
- assert !movie.persisted?
+ assert_not movie.persisted?
end
end
end
diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb
index d5a1d11e12..ae0ce195b3 100644
--- a/activerecord/test/cases/enum_test.rb
+++ b/activerecord/test/cases/enum_test.rb
@@ -44,6 +44,11 @@ class EnumTest < ActiveRecord::TestCase
assert_equal books(:rfr), authors(:david).unpublished_books.first
end
+ test "find via negative scope" do
+ assert Book.not_published.exclude?(@book)
+ assert Book.not_proposed.include?(@book)
+ end
+
test "find via where with values" do
published, written = Book.statuses[:published], Book.statuses[:written]
@@ -265,6 +270,35 @@ class EnumTest < ActiveRecord::TestCase
assert_equal "published", @book.status
end
+ test "invalid definition values raise an ArgumentError" do
+ e = assert_raises(ArgumentError) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum status: [proposed: 1, written: 2, published: 3]
+ end
+ 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
klass = Class.new(ActiveRecord::Base) do
self.table_name = "books"
@@ -409,6 +443,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"
@@ -508,4 +556,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/errors_test.rb b/activerecord/test/cases/errors_test.rb
index b90e6a66c5..0d2be944b5 100644
--- a/activerecord/test/cases/errors_test.rb
+++ b/activerecord/test/cases/errors_test.rb
@@ -8,11 +8,9 @@ class ErrorsTest < ActiveRecord::TestCase
error_klasses = ObjectSpace.each_object(Class).select { |klass| klass < base }
(error_klasses - [ActiveRecord::AmbiguousSourceReflectionForThroughAssociation]).each do |error_klass|
- begin
- error_klass.new.inspect
- rescue ArgumentError
- raise "Instance of #{error_klass} can't be initialized with no arguments"
- end
+ error_klass.new.inspect
+ rescue ArgumentError
+ raise "Instance of #{error_klass} can't be initialized with no arguments"
end
end
end
diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb
index 82cc891970..79a0630193 100644
--- a/activerecord/test/cases/explain_subscriber_test.rb
+++ b/activerecord/test/cases/explain_subscriber_test.rb
@@ -40,7 +40,7 @@ if ActiveRecord::Base.connection.supports_explain?
assert_equal binds, queries[0][1]
end
- def test_collects_nothing_if_the_statement_is_not_whitelisted
+ def test_collects_nothing_if_the_statement_is_not_explainable
SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "SHOW max_identifier_length")
assert_empty queries
end
diff --git a/activerecord/test/cases/filter_attributes_test.rb b/activerecord/test/cases/filter_attributes_test.rb
new file mode 100644
index 0000000000..2f4c9b0ef7
--- /dev/null
+++ b/activerecord/test/cases/filter_attributes_test.rb
@@ -0,0 +1,132 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/admin"
+require "models/admin/user"
+require "models/admin/account"
+require "models/user"
+require "pp"
+
+class FilterAttributesTest < ActiveRecord::TestCase
+ fixtures :"admin/users", :"admin/accounts"
+
+ setup do
+ @previous_filter_attributes = ActiveRecord::Base.filter_attributes
+ ActiveRecord::Base.filter_attributes = [:name]
+ end
+
+ teardown do
+ ActiveRecord::Base.filter_attributes = @previous_filter_attributes
+ end
+
+ test "filter_attributes" do
+ Admin::User.all.each do |user|
+ assert_includes user.inspect, "name: [FILTERED]"
+ assert_equal 1, user.inspect.scan("[FILTERED]").length
+ end
+
+ Admin::Account.all.each do |account|
+ assert_includes account.inspect, "name: [FILTERED]"
+ assert_equal 1, account.inspect.scan("[FILTERED]").length
+ 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]"
+ assert_equal 1, account.inspect.scan("[FILTERED]").length
+ end
+
+ begin
+ Admin::Account.filter_attributes = []
+
+ # Above changes should not impact other models
+ Admin::User.all.each do |user|
+ assert_includes user.inspect, "name: [FILTERED]"
+ assert_equal 1, user.inspect.scan("[FILTERED]").length
+ end
+
+ Admin::Account.all.each do |account|
+ assert_not_includes account.inspect, "name: [FILTERED]"
+ assert_equal 0, account.inspect.scan("[FILTERED]").length
+ end
+ ensure
+ Admin::Account.remove_instance_variable(:@filter_attributes)
+ end
+ end
+
+ test "filter_attributes should not filter nil value" do
+ account = Admin::Account.new
+
+ assert_includes account.inspect, "name: nil"
+ assert_not_includes account.inspect, "name: [FILTERED]"
+ assert_equal 0, account.inspect.scan("[FILTERED]").length
+ end
+
+ test "filter_attributes should handle [FILTERED] value properly" do
+ 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
+
+ test "filter_attributes on pretty_print" do
+ user = admin_users(:david)
+ actual = "".dup
+ PP.pp(user, StringIO.new(actual))
+
+ assert_includes actual, "name: [FILTERED]"
+ assert_equal 1, actual.scan("[FILTERED]").length
+ end
+
+ test "filter_attributes on pretty_print should not filter nil value" do
+ user = Admin::User.new
+ actual = "".dup
+ PP.pp(user, StringIO.new(actual))
+
+ assert_includes actual, "name: nil"
+ 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
+ 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
diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb
index 59af4e6961..66413a98e4 100644
--- a/activerecord/test/cases/finder_respond_to_test.rb
+++ b/activerecord/test/cases/finder_respond_to_test.rb
@@ -12,10 +12,10 @@ class FinderRespondToTest < ActiveRecord::TestCase
end
def test_should_preserve_normal_respond_to_behaviour_and_respond_to_newly_added_method
- class << Topic; self; end.send(:define_method, :method_added_for_finder_respond_to_test) {}
+ Topic.singleton_class.define_method(:method_added_for_finder_respond_to_test) { }
assert_respond_to Topic, :method_added_for_finder_respond_to_test
ensure
- class << Topic; self; end.send(:remove_method, :method_added_for_finder_respond_to_test)
+ Topic.singleton_class.remove_method :method_added_for_finder_respond_to_test
end
def test_should_preserve_normal_respond_to_behaviour_and_respond_to_standard_object_method
@@ -56,6 +56,6 @@ class FinderRespondToTest < ActiveRecord::TestCase
private
def ensure_topic_method_is_not_cached(method_id)
- class << Topic; self; end.send(:remove_method, method_id) if Topic.public_methods.include? method_id
+ Topic.singleton_class.remove_method method_id if Topic.public_methods.include? method_id
end
end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index d85910d9c6..ca114d468e 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -3,6 +3,7 @@
require "cases/helper"
require "models/post"
require "models/author"
+require "models/account"
require "models/categorization"
require "models/comment"
require "models/company"
@@ -20,6 +21,8 @@ require "models/matey"
require "models/dog"
require "models/car"
require "models/tyre"
+require "models/subscriber"
+require "support/stubs/strong_parameters"
class FinderTest < ActiveRecord::TestCase
fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :author_addresses, :customers, :categories, :categorizations, :cars
@@ -167,6 +170,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 +215,35 @@ 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_with_strong_parameters
+ assert_equal false, Subscriber.exists?(ProtectedParams.new(nick: "foo").permit!)
+
+ Subscriber.create!(nick: "foo")
+
+ assert_equal true, Subscriber.exists?(ProtectedParams.new(nick: "foo").permit!)
+
+ assert_raises(ActiveModel::ForbiddenAttributesError) do
+ Subscriber.exists?(ProtectedParams.new(nick: "foo"))
+ end
+ 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 +268,20 @@ 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
+
+ def test_exists_with_distinct_and_offset_and_joins
+ assert Post.left_joins(:comments).distinct.offset(10).exists?
+ assert_not Post.left_joins(:comments).distinct.offset(11).exists?
+ end
+
+ def test_exists_with_distinct_and_offset_and_select
+ assert Post.select(:body).distinct.offset(3).exists?
+ assert_not Post.select(:body).distinct.offset(4).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
@@ -257,6 +293,17 @@ class FinderTest < ActiveRecord::TestCase
assert_equal true, Topic.order(Arel.sql("invalid sql here")).exists?
end
+ def test_exists_with_large_number
+ assert_equal true, Topic.where(id: [1, 9223372036854775808]).exists?
+ assert_equal true, Topic.where(id: 1..9223372036854775808).exists?
+ assert_equal true, Topic.where(id: -9223372036854775809..9223372036854775808).exists?
+ assert_equal false, Topic.where(id: 9223372036854775808..9223372036854775809).exists?
+ assert_equal false, Topic.where(id: -9223372036854775810..-9223372036854775809).exists?
+ assert_equal false, Topic.where(id: 9223372036854775808..1).exists?
+ assert_equal true, Topic.where(id: 1).or(Topic.where(id: 9223372036854775808)).exists?
+ assert_equal true, Topic.where.not(id: 9223372036854775808).exists?
+ end
+
def test_exists_with_joins
assert_equal true, Topic.joins(:replies).where(replies_topics: { approved: true }).order("replies_topics.created_at DESC").exists?
end
@@ -355,17 +402,29 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_on_relation_with_large_number
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Topic.where("1=1").find(9999999999999999999999999999999)
+ end
+ assert_equal topics(:first), Topic.where(id: [1, 9999999999999999999999999999999]).find(1)
+ end
+
+ def test_find_by_on_relation_with_large_number
assert_nil Topic.where("1=1").find_by(id: 9999999999999999999999999999999)
+ assert_equal topics(:first), Topic.where(id: [1, 9999999999999999999999999999999]).find_by(id: 1)
end
def test_find_by_bang_on_relation_with_large_number
assert_raises(ActiveRecord::RecordNotFound) do
Topic.where("1=1").find_by!(id: 9999999999999999999999999999999)
end
+ assert_equal topics(:first), Topic.where(id: [1, 9999999999999999999999999999999]).find_by!(id: 1)
end
def test_find_an_empty_array
- assert_equal [], Topic.find([])
+ empty_array = []
+ result = Topic.find(empty_array)
+ assert_equal [], result
+ assert_not_same empty_array, result
end
def test_find_doesnt_have_implicit_ordering
@@ -403,18 +462,18 @@ class FinderTest < ActiveRecord::TestCase
end
def test_find_by_association_subquery
- author = authors(:david)
- assert_equal author.post, Post.find_by(author: Author.where(id: author))
- assert_equal author.post, Post.find_by(author_id: Author.where(id: author))
+ firm = companies(:first_firm)
+ assert_equal firm.account, Account.find_by(firm: Firm.where(id: firm))
+ assert_equal firm.account, Account.find_by(firm_id: Firm.where(id: firm))
end
def test_find_by_and_where_consistency_with_active_record_instance
- author = authors(:david)
- assert_equal Post.where(author_id: author).take, Post.find_by(author_id: author)
+ firm = companies(:first_firm)
+ assert_equal Account.where(firm_id: firm).take, Account.find_by(firm_id: firm)
end
def test_take
- assert_equal topics(:first), Topic.take
+ assert_equal topics(:first), Topic.where("title = 'The First Topic'").take
end
def test_take_failing
@@ -457,6 +516,7 @@ class FinderTest < ActiveRecord::TestCase
expected = topics(:first)
expected.touch # PostgreSQL changes the default order if no order clause is used
assert_equal expected, Topic.first
+ assert_equal expected, Topic.limit(5).first
end
def test_model_class_responds_to_first_bang
@@ -479,6 +539,7 @@ class FinderTest < ActiveRecord::TestCase
expected = topics(:second)
expected.touch # PostgreSQL changes the default order if no order clause is used
assert_equal expected, Topic.second
+ assert_equal expected, Topic.limit(5).second
end
def test_model_class_responds_to_second_bang
@@ -501,6 +562,7 @@ class FinderTest < ActiveRecord::TestCase
expected = topics(:third)
expected.touch # PostgreSQL changes the default order if no order clause is used
assert_equal expected, Topic.third
+ assert_equal expected, Topic.limit(5).third
end
def test_model_class_responds_to_third_bang
@@ -523,6 +585,7 @@ class FinderTest < ActiveRecord::TestCase
expected = topics(:fourth)
expected.touch # PostgreSQL changes the default order if no order clause is used
assert_equal expected, Topic.fourth
+ assert_equal expected, Topic.limit(5).fourth
end
def test_model_class_responds_to_fourth_bang
@@ -545,6 +608,7 @@ class FinderTest < ActiveRecord::TestCase
expected = topics(:fifth)
expected.touch # PostgreSQL changes the default order if no order clause is used
assert_equal expected, Topic.fifth
+ assert_equal expected, Topic.limit(5).fifth
end
def test_model_class_responds_to_fifth_bang
@@ -707,6 +771,24 @@ class FinderTest < ActiveRecord::TestCase
assert_equal comments.limit(2).to_a.first(3), comments.limit(2).first(3)
end
+ def test_first_have_determined_order_by_default
+ expected = [companies(:second_client), companies(:another_client)]
+ clients = Client.where(name: expected.map(&:name))
+
+ assert_equal expected, clients.first(2)
+ 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)
@@ -727,8 +809,8 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(ActiveModel::MissingAttributeError) { topic.title? }
assert_nil topic.read_attribute("title")
assert_equal "David", topic.author_name
- assert !topic.attribute_present?("title")
- assert !topic.attribute_present?(:title)
+ assert_not topic.attribute_present?("title")
+ assert_not topic.attribute_present?(:title)
assert topic.attribute_present?("author_name")
assert_respond_to topic, "author_name"
end
@@ -894,6 +976,7 @@ class FinderTest < ActiveRecord::TestCase
assert_kind_of Money, zaphod_balance
found_customers = Customer.where(balance: [david_balance, zaphod_balance])
assert_equal [customers(:david), customers(:zaphod)], found_customers.sort_by(&:id)
+ assert_equal Customer.where(balance: [david_balance.amount, zaphod_balance.amount]).to_sql, found_customers.to_sql
end
def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_aggregate
@@ -931,6 +1014,24 @@ class FinderTest < ActiveRecord::TestCase
assert_equal customers(:david), found_customer
end
+ def test_hash_condition_find_nil_with_aggregate_having_one_mapping
+ assert_nil customers(:zaphod).gps_location
+ found_customer = Customer.where(gps_location: nil, name: customers(:zaphod).name).first
+ assert_equal customers(:zaphod), found_customer
+ end
+
+ def test_hash_condition_find_nil_with_aggregate_having_multiple_mappings
+ customers(:david).update(address: nil)
+ assert_nil customers(:david).address_street
+ assert_nil customers(:david).address_city
+ found_customer = Customer.where(address: nil, name: customers(:david).name).first
+ assert_equal customers(:david), found_customer
+ end
+
+ def test_hash_condition_find_empty_array_with_aggregate_having_multiple_mappings
+ assert_nil Customer.where(address: []).first
+ end
+
def test_condition_utc_time_interpolation_with_default_timezone_local
with_env_tz "America/New_York" do
with_timezone_config default: :local do
@@ -1072,7 +1173,7 @@ class FinderTest < ActiveRecord::TestCase
def test_dynamic_finder_on_one_attribute_with_conditions_returns_same_results_after_caching
# ensure this test can run independently of order
- class << Account; self; end.send(:remove_method, :find_by_credit_limit) if Account.public_methods.include?(:find_by_credit_limit)
+ Account.singleton_class.remove_method :find_by_credit_limit if Account.public_methods.include?(:find_by_credit_limit)
a = Account.where("firm_id = ?", 6).find_by_credit_limit(50)
assert_equal a, Account.where("firm_id = ?", 6).find_by_credit_limit(50) # find_by_credit_limit has been cached
end
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index 184b750161..0cb868da6e 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
require "cases/helper"
+require "support/connection_helper"
require "models/admin"
require "models/admin/account"
require "models/admin/randomly_named_c1"
@@ -32,6 +33,8 @@ require "models/treasure"
require "tempfile"
class FixturesTest < ActiveRecord::TestCase
+ include ConnectionHelper
+
self.use_instantiated_fixtures = true
self.use_transactional_tests = false
@@ -70,14 +73,12 @@ class FixturesTest < ActiveRecord::TestCase
if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
def test_bulk_insert
- begin
- subscriber = InsertQuerySubscriber.new
- subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber)
- create_fixtures("bulbs")
- assert_equal 1, subscriber.events.size, "It takes one INSERT query to insert two fixtures"
- ensure
- ActiveSupport::Notifications.unsubscribe(subscription)
- end
+ subscriber = InsertQuerySubscriber.new
+ subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber)
+ create_fixtures("bulbs")
+ assert_equal 1, subscriber.events.size, "It takes one INSERT query to insert two fixtures"
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscription)
end
def test_bulk_insert_multiple_table_with_a_multi_statement_query
@@ -114,24 +115,111 @@ class FixturesTest < ActiveRecord::TestCase
end
end
end
+
+ def test_bulk_insert_with_a_multi_statement_query_in_a_nested_transaction
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a"] },
+ ]
+ }
+
+ assert_difference "TrafficLight.count" do
+ ActiveRecord::Base.transaction do
+ conn = ActiveRecord::Base.connection
+ assert_equal 1, conn.open_transactions
+ conn.insert_fixtures_set(fixtures)
+ assert_equal 1, conn.open_transactions
+ end
+ end
+ end
end
if current_adapter?(:Mysql2Adapter)
+ def test_bulk_insert_with_multi_statements_enabled
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(
+ orig_connection.merge(flags: %w[MULTI_STATEMENTS])
+ )
+
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a"] },
+ ]
+ }
+
+ ActiveRecord::Base.connection.stub(:supports_set_server_option?, false) do
+ assert_nothing_raised do
+ conn = ActiveRecord::Base.connection
+ conn.execute("SELECT 1; SELECT 2;")
+ conn.raw_connection.abandon_results!
+ end
+
+ assert_difference "TrafficLight.count" do
+ ActiveRecord::Base.transaction do
+ conn = ActiveRecord::Base.connection
+ assert_equal 1, conn.open_transactions
+ conn.insert_fixtures_set(fixtures)
+ assert_equal 1, conn.open_transactions
+ end
+ end
+
+ assert_nothing_raised do
+ conn = ActiveRecord::Base.connection
+ conn.execute("SELECT 1; SELECT 2;")
+ conn.raw_connection.abandon_results!
+ end
+ end
+ end
+ end
+
+ def test_bulk_insert_with_multi_statements_disabled
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(
+ orig_connection.merge(flags: [])
+ )
+
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a"] },
+ ]
+ }
+
+ ActiveRecord::Base.connection.stub(:supports_set_server_option?, false) do
+ assert_raises(ActiveRecord::StatementInvalid) do
+ conn = ActiveRecord::Base.connection
+ conn.execute("SELECT 1; SELECT 2;")
+ conn.raw_connection.abandon_results!
+ end
+
+ assert_difference "TrafficLight.count" do
+ conn = ActiveRecord::Base.connection
+ conn.insert_fixtures_set(fixtures)
+ end
+
+ assert_raises(ActiveRecord::StatementInvalid) do
+ conn = ActiveRecord::Base.connection
+ conn.execute("SELECT 1; SELECT 2;")
+ conn.raw_connection.abandon_results!
+ end
+ end
+ end
+ end
+
def test_insert_fixtures_set_raises_an_error_when_max_allowed_packet_is_smaller_than_fixtures_set_size
conn = ActiveRecord::Base.connection
mysql_margin = 2
packet_size = 1024
- bytes_needed_to_have_a_1024_bytes_fixture = 858
+ bytes_needed_to_have_a_1024_bytes_fixture = 906
fixtures = {
"traffic_lights" => [
{ "location" => "US", "state" => ["NY"], "long_state" => ["a" * bytes_needed_to_have_a_1024_bytes_fixture] },
]
}
- conn.stubs(:max_allowed_packet).returns(packet_size - mysql_margin)
-
- error = assert_raises(ActiveRecord::ActiveRecordError) { conn.insert_fixtures_set(fixtures) }
- assert_match(/Fixtures set is too large #{packet_size}\./, error.message)
+ conn.stub(:max_allowed_packet, packet_size - mysql_margin) do
+ error = assert_raises(ActiveRecord::ActiveRecordError) { conn.insert_fixtures_set(fixtures) }
+ assert_match(/Fixtures set is too large #{packet_size}\./, error.message)
+ end
end
def test_insert_fixture_set_when_max_allowed_packet_is_bigger_than_fixtures_set_size
@@ -143,10 +231,10 @@ class FixturesTest < ActiveRecord::TestCase
]
}
- conn.stubs(:max_allowed_packet).returns(packet_size)
-
- assert_difference "TrafficLight.count" do
- conn.insert_fixtures_set(fixtures)
+ conn.stub(:max_allowed_packet, packet_size) do
+ assert_difference "TrafficLight.count" do
+ conn.insert_fixtures_set(fixtures)
+ end
end
end
@@ -164,12 +252,13 @@ class FixturesTest < ActiveRecord::TestCase
]
}
- conn.stubs(:max_allowed_packet).returns(packet_size)
+ conn.stub(:max_allowed_packet, packet_size) do
+ conn.insert_fixtures_set(fixtures)
- conn.insert_fixtures_set(fixtures)
- assert_equal 2, subscriber.events.size
- assert_operator subscriber.events.first.bytesize, :<, packet_size
- assert_operator subscriber.events.second.bytesize, :<, packet_size
+ assert_equal 2, subscriber.events.size
+ assert_operator subscriber.events.first.bytesize, :<, packet_size
+ assert_operator subscriber.events.second.bytesize, :<, packet_size
+ end
ensure
ActiveSupport::Notifications.unsubscribe(subscription)
end
@@ -188,10 +277,10 @@ class FixturesTest < ActiveRecord::TestCase
]
}
- conn.stubs(:max_allowed_packet).returns(packet_size)
-
- assert_difference ["TrafficLight.count", "Comment.count"], +1 do
- conn.insert_fixtures_set(fixtures)
+ conn.stub(:max_allowed_packet, packet_size) do
+ assert_difference ["TrafficLight.count", "Comment.count"], +1 do
+ conn.insert_fixtures_set(fixtures)
+ end
end
assert_equal 1, subscriber.events.size
ensure
@@ -212,20 +301,6 @@ class FixturesTest < ActiveRecord::TestCase
assert_equal fixtures, result.to_a
end
- def test_deprecated_insert_fixtures
- fixtures = [
- { "name" => "first", "wheels_count" => 2 },
- { "name" => "second", "wheels_count" => 3 }
- ]
- conn = ActiveRecord::Base.connection
- conn.delete("DELETE FROM aircraft")
- assert_deprecated do
- conn.insert_fixtures(fixtures, "aircraft")
- end
- result = conn.select_all("SELECT name, wheels_count FROM aircraft ORDER BY id")
- assert_equal fixtures, result.to_a
- end
-
def test_broken_yaml_exception
badyaml = Tempfile.new ["foo", ".yml"]
badyaml.write "a: : "
@@ -382,11 +457,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
@@ -396,14 +471,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
@@ -411,7 +486,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
@@ -421,11 +496,7 @@ class FixturesTest < ActiveRecord::TestCase
ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "parrots")
end
- if current_adapter?(:SQLite3Adapter)
- assert_equal(%(table "parrots" has no column named "arrr".), e.message)
- else
- assert_equal(%(table "parrots" has no columns named "arrr", "foobar".), e.message)
- end
+ assert_equal(%(table "parrots" has no columns named "arrr", "foobar".), e.message)
end
def test_yaml_file_with_symbol_columns
@@ -434,7 +505,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
@@ -505,7 +576,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
@@ -523,18 +594,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
@@ -592,14 +667,14 @@ class FixturesWithoutInstantiationTest < ActiveRecord::TestCase
fixtures :topics, :developers, :accounts
def test_without_complete_instantiation
- assert !defined?(@first)
- assert !defined?(@topics)
- assert !defined?(@developers)
- assert !defined?(@accounts)
+ assert_not defined?(@first)
+ assert_not defined?(@topics)
+ assert_not defined?(@developers)
+ assert_not defined?(@accounts)
end
def test_fixtures_from_root_yml_without_instantiation
- assert !defined?(@unknown), "@unknown is not defined"
+ assert_not defined?(@unknown), "@unknown is not defined"
end
def test_visibility_of_accessor_method
@@ -634,7 +709,7 @@ class FixturesWithoutInstanceInstantiationTest < ActiveRecord::TestCase
fixtures :topics, :developers, :accounts
def test_without_instance_instantiation
- assert !defined?(@first), "@first is not defined"
+ assert_not defined?(@first), "@first is not defined"
end
end
@@ -833,44 +908,58 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase
self.use_instantiated_fixtures = false
def test_transaction_created_on_connection_notification
- connection = stub(transaction_open?: false)
- connection.expects(:begin_transaction).with(joinable: false)
- pool = connection.stubs(:pool).returns(ActiveRecord::ConnectionAdapters::ConnectionPool.new(ActiveRecord::Base.connection_pool.spec))
- pool.stubs(:lock_thread=).with(false)
- fire_connection_notification(connection)
+ connection = Class.new do
+ attr_accessor :pool
+
+ def transaction_open?; end
+ def begin_transaction(*args); end
+ def rollback_transaction(*args); end
+ end.new
+
+ connection.pool = Class.new do
+ def lock_thread=(lock_thread); end
+ end.new
+
+ assert_called_with(connection, :begin_transaction, [joinable: false, _lazy: false]) do
+ fire_connection_notification(connection)
+ end
end
def test_notification_established_transactions_are_rolled_back
- # Mocha is not thread-safe so define our own stub to test
connection = Class.new do
attr_accessor :rollback_transaction_called
attr_accessor :pool
+
def transaction_open?; true; end
def begin_transaction(*args); end
def rollback_transaction(*args)
@rollback_transaction_called = true
end
end.new
+
connection.pool = Class.new do
- def lock_thread=(lock_thread); false; end
+ def lock_thread=(lock_thread); end
end.new
+
fire_connection_notification(connection)
teardown_fixtures
+
assert(connection.rollback_transaction_called, "Expected <mock connection>#rollback_transaction to be called but was not")
end
private
def fire_connection_notification(connection)
- ActiveRecord::Base.connection_handler.stubs(:retrieve_connection).with("book").returns(connection)
- message_bus = ActiveSupport::Notifications.instrumenter
- payload = {
- spec_name: "book",
- config: nil,
- connection_id: connection.object_id
- }
+ assert_called_with(ActiveRecord::Base.connection_handler, :retrieve_connection, ["book"], returns: connection) do
+ message_bus = ActiveSupport::Notifications.instrumenter
+ payload = {
+ spec_name: "book",
+ config: nil,
+ connection_id: connection.object_id
+ }
- message_bus.instrument("!connection.active_record", payload) {}
+ message_bus.instrument("!connection.active_record", payload) { }
+ end
end
end
@@ -1082,13 +1171,13 @@ class FoxyFixturesTest < ActiveRecord::TestCase
def test_supports_inline_habtm
assert(parrots(:george).treasures.include?(treasures(:diamond)))
assert(parrots(:george).treasures.include?(treasures(:sapphire)))
- assert(!parrots(:george).treasures.include?(treasures(:ruby)))
+ assert_not(parrots(:george).treasures.include?(treasures(:ruby)))
end
def test_supports_inline_habtm_with_specified_id
assert(parrots(:polly).treasures.include?(treasures(:ruby)))
assert(parrots(:polly).treasures.include?(treasures(:sapphire)))
- assert(!parrots(:polly).treasures.include?(treasures(:diamond)))
+ assert_not(parrots(:polly).treasures.include?(treasures(:diamond)))
end
def test_supports_yaml_arrays
@@ -1239,3 +1328,53 @@ class SameNameDifferentDatabaseFixturesTest < ActiveRecord::TestCase
assert_kind_of OtherDog, other_dogs(:lassie)
end
end
+
+class NilFixturePathTest < ActiveRecord::TestCase
+ test "raises an error when all fixtures loaded" do
+ error = assert_raises(StandardError) do
+ TestCase = Class.new(ActiveRecord::TestCase)
+ TestCase.class_eval do
+ self.fixture_path = nil
+ fixtures :all
+ end
+ end
+ assert_equal <<~MSG.squish, error.message
+ No fixture path found.
+ Please set `NilFixturePathTest::TestCase.fixture_path`.
+ MSG
+ end
+end
+
+class MultipleDatabaseFixturesTest < ActiveRecord::TestCase
+ test "enlist_fixture_connections ensures multiple databases share a connection pool" do
+ with_temporary_connection_pool do
+ ActiveRecord::Base.connects_to database: { writing: :arunit, reading: :arunit2 }
+
+ rw_conn = ActiveRecord::Base.connection
+ ro_conn = ActiveRecord::Base.connection_handlers[:reading].connection_pool_list.first.connection
+
+ assert_not_equal rw_conn, ro_conn
+
+ enlist_fixture_connections
+
+ rw_conn = ActiveRecord::Base.connection
+ ro_conn = ActiveRecord::Base.connection_handlers[:reading].connection_pool_list.first.connection
+
+ assert_equal rw_conn, ro_conn
+ end
+ ensure
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.connection_handler }
+ end
+
+ private
+
+ def with_temporary_connection_pool
+ old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name)
+ new_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new ActiveRecord::Base.connection_pool.spec
+ ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = new_pool
+
+ yield
+ ensure
+ ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = old_pool
+ end
+end
diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb
index 101fa118c8..e7e31b6d2d 100644
--- a/activerecord/test/cases/forbidden_attributes_protection_test.rb
+++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb
@@ -1,48 +1,12 @@
# frozen_string_literal: true
require "cases/helper"
-require "active_support/core_ext/hash/indifferent_access"
-
require "models/company"
require "models/person"
require "models/ship"
require "models/ship_part"
require "models/treasure"
-
-class ProtectedParams
- attr_accessor :permitted
- alias :permitted? :permitted
-
- delegate :keys, :key?, :has_key?, :empty?, to: :@parameters
-
- def initialize(attributes)
- @parameters = attributes.with_indifferent_access
- @permitted = false
- end
-
- def permit!
- @permitted = true
- self
- end
-
- def [](key)
- @parameters[key]
- end
-
- def to_h
- @parameters
- end
-
- def stringify_keys
- dup
- end
-
- def dup
- super.tap do |duplicate|
- duplicate.instance_variable_set :@permitted, @permitted
- end
- end
-end
+require "support/stubs/strong_parameters"
class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase
def test_forbidden_attributes_cannot_be_used_for_mass_assignment
diff --git a/activerecord/test/cases/habtm_destroy_order_test.rb b/activerecord/test/cases/habtm_destroy_order_test.rb
index b15e1b48c4..9dbd339fe7 100644
--- a/activerecord/test/cases/habtm_destroy_order_test.rb
+++ b/activerecord/test/cases/habtm_destroy_order_test.rb
@@ -30,23 +30,21 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase
test "not destroying a student with lessons leaves student<=>lesson association intact" do
# test a normal before_destroy doesn't destroy the habtm joins
- begin
- sicp = Lesson.new(name: "SICP")
- ben = Student.new(name: "Ben Bitdiddle")
- # add a before destroy to student
- Student.class_eval do
- before_destroy do
- raise ActiveRecord::Rollback unless lessons.empty?
- end
+ sicp = Lesson.new(name: "SICP")
+ ben = Student.new(name: "Ben Bitdiddle")
+ # add a before destroy to student
+ Student.class_eval do
+ before_destroy do
+ raise ActiveRecord::Rollback unless lessons.empty?
end
- ben.lessons << sicp
- ben.save!
- ben.destroy
- assert_not_empty ben.reload.lessons
- ensure
- # get rid of it so Student is still like it was
- Student.reset_callbacks(:destroy)
end
+ ben.lessons << sicp
+ ben.save!
+ ben.destroy
+ assert_not_empty ben.reload.lessons
+ ensure
+ # get rid of it so Student is still like it was
+ Student.reset_callbacks(:destroy)
end
test "not destroying a lesson with students leaves student<=>lesson association intact" do
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 66f11fe5bd..543a0aeb39 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -48,8 +48,27 @@ def mysql_enforcing_gtid_consistency?
current_adapter?(:Mysql2Adapter) && "ON" == ActiveRecord::Base.connection.show_variable("enforce_gtid_consistency")
end
-def supports_savepoints?
- ActiveRecord::Base.connection.supports_savepoints?
+def supports_default_expression?
+ if current_adapter?(:PostgreSQLAdapter)
+ true
+ elsif current_adapter?(:Mysql2Adapter)
+ conn = ActiveRecord::Base.connection
+ !conn.mariadb? && conn.database_version >= "8.0.13"
+ end
+end
+
+%w[
+ supports_savepoints?
+ supports_partial_index?
+ supports_insert_returning?
+ supports_insert_on_duplicate_skip?
+ supports_insert_on_duplicate_update?
+ supports_insert_conflict_target?
+ supports_optimizer_hints?
+].each do |method_name|
+ define_method method_name do
+ ActiveRecord::Base.connection.public_send(method_name)
+ end
end
def with_env_tz(new_tz = "US/Eastern")
@@ -184,4 +203,4 @@ module InTimeZone
end
end
-require "mocha/minitest" # FIXME: stop using mocha
+require_relative "../../../tools/test_common"
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/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index 7a5c06b894..629167e9ed 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -91,7 +91,6 @@ class InheritanceTest < ActiveRecord::TestCase
end
ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise e }) do
-
exception = assert_raises NameError do
Company.send :compute_type, "InvalidModel"
end
@@ -163,7 +162,7 @@ class InheritanceTest < ActiveRecord::TestCase
assert_not_predicate ActiveRecord::Base, :descends_from_active_record?
assert AbstractCompany.descends_from_active_record?, "AbstractCompany should descend from ActiveRecord::Base"
assert Company.descends_from_active_record?, "Company should descend from ActiveRecord::Base"
- assert !Class.new(Company).descends_from_active_record?, "Company subclass should not descend from ActiveRecord::Base"
+ assert_not Class.new(Company).descends_from_active_record?, "Company subclass should not descend from ActiveRecord::Base"
end
def test_abstract_class
@@ -241,7 +240,7 @@ class InheritanceTest < ActiveRecord::TestCase
cabbage = vegetable.becomes!(Cabbage)
assert_equal "Cabbage", cabbage.custom_type
- vegetable = cabbage.becomes!(Vegetable)
+ cabbage.becomes!(Vegetable)
assert_nil cabbage.custom_type
end
@@ -515,10 +514,12 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase
# Should fail without FirmOnTheFly in the type condition.
assert_raise(ActiveRecord::RecordNotFound) { Firm.find(foo.id) }
+ assert_raise(ActiveRecord::RecordNotFound) { Firm.find_by!(id: foo.id) }
# Nest FirmOnTheFly in the test case where Dependencies won't see it.
self.class.const_set :FirmOnTheFly, Class.new(Firm)
assert_raise(ActiveRecord::SubclassNotFound) { Firm.find(foo.id) }
+ assert_raise(ActiveRecord::SubclassNotFound) { Firm.find_by!(id: foo.id) }
# Nest FirmOnTheFly in Firm where Dependencies will see it.
# This is analogous to nesting models in a migration.
@@ -527,6 +528,7 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase
# And instantiate will find the existing constant rather than trying
# to require firm_on_the_fly.
assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find(foo.id) }
+ assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find_by!(id: foo.id) }
end
end
@@ -655,7 +657,7 @@ class InheritanceAttributeMappingTest < ActiveRecord::TestCase
assert_equal ["omg_inheritance_attribute_mapping_test/company"], ActiveRecord::Base.connection.select_values("SELECT sponsorable_type FROM sponsors")
- sponsor = Sponsor.first
+ sponsor = Sponsor.find(sponsor.id)
assert_equal startup, sponsor.sponsorable
end
end
diff --git a/activerecord/test/cases/insert_all_test.rb b/activerecord/test/cases/insert_all_test.rb
new file mode 100644
index 0000000000..f24c63031c
--- /dev/null
+++ b/activerecord/test/cases/insert_all_test.rb
@@ -0,0 +1,276 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/book"
+
+class ReadonlyNameBook < Book
+ attr_readonly :name
+end
+
+class InsertAllTest < ActiveRecord::TestCase
+ fixtures :books
+
+ def test_insert
+ skip unless supports_insert_on_duplicate_skip?
+
+ id = 1_000_000
+
+ assert_difference "Book.count", +1 do
+ Book.insert(id: id, name: "Rework", author_id: 1)
+ end
+
+ Book.upsert(id: id, name: "Remote", author_id: 1)
+
+ assert_equal "Remote", Book.find(id).name
+ end
+
+ def test_insert!
+ assert_difference "Book.count", +1 do
+ Book.insert! name: "Rework", author_id: 1
+ end
+ end
+
+ def test_insert_all
+ assert_difference "Book.count", +10 do
+ Book.insert_all! [
+ { name: "Rework", author_id: 1 },
+ { name: "Patterns of Enterprise Application Architecture", author_id: 1 },
+ { name: "Design of Everyday Things", author_id: 1 },
+ { name: "Practical Object-Oriented Design in Ruby", author_id: 1 },
+ { name: "Clean Code", author_id: 1 },
+ { name: "Ruby Under a Microscope", author_id: 1 },
+ { name: "The Principles of Product Development Flow", author_id: 1 },
+ { name: "Peopleware", author_id: 1 },
+ { name: "About Face", author_id: 1 },
+ { name: "Eloquent Ruby", author_id: 1 },
+ ]
+ end
+ end
+
+ def test_insert_all_should_handle_empty_arrays
+ assert_raise ArgumentError do
+ Book.insert_all! []
+ end
+ end
+
+ def test_insert_all_raises_on_duplicate_records
+ assert_raise ActiveRecord::RecordNotUnique do
+ Book.insert_all! [
+ { name: "Rework", author_id: 1 },
+ { name: "Patterns of Enterprise Application Architecture", author_id: 1 },
+ { name: "Agile Web Development with Rails", author_id: 1 },
+ ]
+ end
+ end
+
+ def test_insert_all_returns_ActiveRecord_Result
+ result = Book.insert_all! [{ name: "Rework", author_id: 1 }]
+ assert_kind_of ActiveRecord::Result, result
+ end
+
+ def test_insert_all_returns_primary_key_if_returning_is_supported
+ skip unless supports_insert_returning?
+
+ result = Book.insert_all! [{ name: "Rework", author_id: 1 }]
+ assert_equal %w[ id ], result.columns
+ end
+
+ def test_insert_all_returns_nothing_if_returning_is_empty
+ skip unless supports_insert_returning?
+
+ result = Book.insert_all! [{ name: "Rework", author_id: 1 }], returning: []
+ assert_equal [], result.columns
+ end
+
+ def test_insert_all_returns_nothing_if_returning_is_false
+ skip unless supports_insert_returning?
+
+ result = Book.insert_all! [{ name: "Rework", author_id: 1 }], returning: false
+ assert_equal [], result.columns
+ end
+
+ def test_insert_all_returns_requested_fields
+ skip unless supports_insert_returning?
+
+ result = Book.insert_all! [{ name: "Rework", author_id: 1 }], returning: [:id, :name]
+ assert_equal %w[ Rework ], result.pluck("name")
+ end
+
+ def test_insert_all_can_skip_duplicate_records
+ skip unless supports_insert_on_duplicate_skip?
+
+ assert_no_difference "Book.count" do
+ Book.insert_all [{ id: 1, name: "Agile Web Development with Rails" }]
+ end
+ end
+
+ def test_insert_all_with_skip_duplicates_and_autonumber_id_not_given
+ skip unless supports_insert_on_duplicate_skip?
+
+ assert_difference "Book.count", 1 do
+ # These two books are duplicates according to an index on %i[author_id name]
+ # but their IDs are not specified so they will be assigned different IDs
+ # by autonumber. We will get an exception from MySQL if we attempt to skip
+ # one of these records by assigning its ID.
+ Book.insert_all [
+ { author_id: 8, name: "Refactoring" },
+ { author_id: 8, name: "Refactoring" }
+ ]
+ end
+ end
+
+ def test_insert_all_with_skip_duplicates_and_autonumber_id_given
+ skip unless supports_insert_on_duplicate_skip?
+
+ assert_difference "Book.count", 1 do
+ Book.insert_all [
+ { id: 200, author_id: 8, name: "Refactoring" },
+ { id: 201, author_id: 8, name: "Refactoring" }
+ ]
+ end
+ end
+
+ def test_skip_duplicates_strategy_does_not_secretly_upsert
+ skip unless supports_insert_on_duplicate_skip?
+
+ book = Book.create!(author_id: 8, name: "Refactoring", format: "EXPECTED")
+
+ assert_no_difference "Book.count" do
+ Book.insert(author_id: 8, name: "Refactoring", format: "UNEXPECTED")
+ end
+
+ assert_equal "EXPECTED", book.reload.format
+ end
+
+ def test_insert_all_will_raise_if_duplicates_are_skipped_only_for_a_certain_conflict_target
+ skip unless supports_insert_on_duplicate_skip? && supports_insert_conflict_target?
+
+ assert_raise ActiveRecord::RecordNotUnique do
+ Book.insert_all [{ id: 1, name: "Agile Web Development with Rails" }],
+ unique_by: :index_books_on_author_id_and_name
+ end
+ end
+
+ def test_insert_all_and_upsert_all_with_index_finding_options
+ skip unless supports_insert_conflict_target?
+
+ assert_difference "Book.count", +3 do
+ Book.insert_all [{ name: "Rework", author_id: 1 }], unique_by: :isbn
+ Book.insert_all [{ name: "Remote", author_id: 1 }], unique_by: %i( author_id name )
+ Book.insert_all [{ name: "Renote", author_id: 1 }], unique_by: :index_books_on_isbn
+ end
+
+ assert_raise ActiveRecord::RecordNotUnique do
+ Book.upsert_all [{ name: "Rework", author_id: 1 }], unique_by: :isbn
+ end
+ end
+
+ def test_insert_all_and_upsert_all_raises_when_index_is_missing
+ skip unless supports_insert_conflict_target?
+
+ [ :cats, %i( author_id isbn ), :author_id ].each do |missing_or_non_unique_by|
+ error = assert_raises ArgumentError do
+ Book.insert_all [{ name: "Rework", author_id: 1 }], unique_by: missing_or_non_unique_by
+ end
+ assert_match "No unique index", error.message
+
+ error = assert_raises ArgumentError do
+ Book.upsert_all [{ name: "Rework", author_id: 1 }], unique_by: missing_or_non_unique_by
+ end
+ assert_match "No unique index", error.message
+ end
+ end
+
+ def test_insert_logs_message_including_model_name
+ skip unless supports_insert_conflict_target?
+
+ capture_log_output do |output|
+ Book.insert(name: "Rework", author_id: 1)
+ assert_match "Book Insert", output.string
+ end
+ end
+
+ def test_insert_all_logs_message_including_model_name
+ skip unless supports_insert_conflict_target?
+
+ capture_log_output do |output|
+ Book.insert_all [{ name: "Remote", author_id: 1 }, { name: "Renote", author_id: 1 }]
+ assert_match "Book Bulk Insert", output.string
+ end
+ end
+
+ def test_upsert_logs_message_including_model_name
+ skip unless supports_insert_on_duplicate_update?
+
+ capture_log_output do |output|
+ Book.upsert(name: "Remote", author_id: 1)
+ assert_match "Book Upsert", output.string
+ end
+ end
+
+ def test_upsert_all_logs_message_including_model_name
+ skip unless supports_insert_on_duplicate_update?
+
+ capture_log_output do |output|
+ Book.upsert_all [{ name: "Remote", author_id: 1 }, { name: "Renote", author_id: 1 }]
+ assert_match "Book Bulk Upsert", output.string
+ end
+ end
+
+ def test_upsert_all_updates_existing_records
+ skip unless supports_insert_on_duplicate_update?
+
+ new_name = "Agile Web Development with Rails, 4th Edition"
+ Book.upsert_all [{ id: 1, name: new_name }]
+ assert_equal new_name, Book.find(1).name
+ end
+
+ def test_upsert_all_does_not_update_readonly_attributes
+ skip unless supports_insert_on_duplicate_update?
+
+ new_name = "Agile Web Development with Rails, 4th Edition"
+ ReadonlyNameBook.upsert_all [{ id: 1, name: new_name }]
+ assert_not_equal new_name, Book.find(1).name
+ end
+
+ def test_upsert_all_does_not_update_primary_keys
+ skip unless supports_insert_on_duplicate_update? && supports_insert_conflict_target?
+
+ Book.upsert_all [{ id: 101, name: "Perelandra", author_id: 7 }]
+ Book.upsert_all [{ id: 103, name: "Perelandra", author_id: 7, isbn: "1974522598" }],
+ unique_by: :index_books_on_author_id_and_name
+
+ book = Book.find_by(name: "Perelandra")
+ assert_equal 101, book.id, "Should not have updated the ID"
+ assert_equal "1974522598", book.isbn, "Should have updated the isbn"
+ end
+
+ def test_upsert_all_does_not_perform_an_upsert_if_a_partial_index_doesnt_apply
+ skip unless supports_insert_on_duplicate_update? && supports_insert_conflict_target? && supports_partial_index?
+
+ Book.upsert_all [{ name: "Out of the Silent Planet", author_id: 7, isbn: "1974522598", published_on: Date.new(1938, 4, 1) }]
+ Book.upsert_all [{ name: "Perelandra", author_id: 7, isbn: "1974522598" }],
+ unique_by: :index_books_on_isbn
+
+ assert_equal ["Out of the Silent Planet", "Perelandra"], Book.where(isbn: "1974522598").order(:name).pluck(:name)
+ end
+
+ def test_insert_all_raises_on_unknown_attribute
+ assert_raise ActiveRecord::UnknownAttributeError do
+ Book.insert_all! [{ unknown_attribute: "Test" }]
+ end
+ end
+
+ private
+
+ def capture_log_output
+ output = StringIO.new
+ old_logger, ActiveRecord::Base.logger = ActiveRecord::Base.logger, ActiveSupport::Logger.new(output)
+
+ begin
+ yield output
+ ensure
+ ActiveRecord::Base.logger = old_logger
+ end
+ end
+end
diff --git a/activerecord/test/cases/instrumentation_test.rb b/activerecord/test/cases/instrumentation_test.rb
index e6e8468757..06382b6c7c 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|
@@ -37,8 +41,8 @@ module ActiveRecord
assert_equal "Book Update", event.payload[:name]
end
end
- book = Book.create(name: "test book")
- book.update_attribute(:name, "new name")
+ book = Book.create(name: "test book", format: "paperback")
+ book.update_attribute(:format, "ebook")
ensure
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
end
@@ -50,8 +54,8 @@ module ActiveRecord
assert_equal "Book Update All", event.payload[:name]
end
end
- Book.create(name: "test book")
- Book.update_all(name: "new name")
+ Book.create(name: "test book", format: "paperback")
+ Book.update_all(format: "ebook")
ensure
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
end
diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb
index 36cd63c4d4..4185e8d682 100644
--- a/activerecord/test/cases/integration_test.rb
+++ b/activerecord/test/cases/integration_test.rb
@@ -157,78 +157,87 @@ class IntegrationTest < ActiveRecord::TestCase
skip("Subsecond precision is not supported") unless subsecond_precision_supported?
dev = Developer.first
key = dev.cache_key
- dev.touch
+ travel_to dev.updated_at + 0.000001 do
+ dev.touch
+ end
assert_not_equal key, dev.cache_key
end
def test_cache_key_format_is_not_too_precise
- skip("Subsecond precision is not supported") unless subsecond_precision_supported?
dev = Developer.first
dev.touch
key = dev.cache_key
assert_equal key, dev.reload.cache_key
end
- def test_named_timestamps_for_cache_key
- assert_deprecated do
- owner = owners(:blackbeard)
- assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at)
+ def test_cache_version_format_is_precise_enough
+ skip("Subsecond precision is not supported") unless subsecond_precision_supported?
+ with_cache_versioning do
+ dev = Developer.first
+ version = dev.cache_version.to_param
+ travel_to Developer.first.updated_at + 0.000001 do
+ dev.touch
+ end
+ assert_not_equal version, dev.cache_version.to_param
end
end
- def test_cache_key_when_named_timestamp_is_nil
- assert_deprecated do
- owner = owners(:blackbeard)
- owner.happy_at = nil
- assert_equal "owners/#{owner.id}", owner.cache_key(:happy_at)
+ def test_cache_version_format_is_not_too_precise
+ with_cache_versioning do
+ dev = Developer.first
+ dev.touch
+ key = dev.cache_version.to_param
+ assert_equal key, dev.reload.cache_version.to_param
end
end
def test_cache_key_is_stable_with_versioning_on
- Developer.cache_versioning = true
-
- developer = Developer.first
- first_key = developer.cache_key
+ with_cache_versioning do
+ developer = Developer.first
+ first_key = developer.cache_key
- developer.touch
- second_key = developer.cache_key
+ developer.touch
+ second_key = developer.cache_key
- assert_equal first_key, second_key
- ensure
- Developer.cache_versioning = false
+ assert_equal first_key, second_key
+ end
end
def test_cache_version_changes_with_versioning_on
- Developer.cache_versioning = true
+ with_cache_versioning do
+ developer = Developer.first
+ first_version = developer.cache_version
- developer = Developer.first
- first_version = developer.cache_version
+ travel 10.seconds do
+ developer.touch
+ end
- travel 10.seconds do
- developer.touch
- end
-
- second_version = developer.cache_version
+ second_version = developer.cache_version
- assert_not_equal first_version, second_version
- ensure
- Developer.cache_versioning = false
+ assert_not_equal first_version, second_version
+ end
end
def test_cache_key_retains_version_when_custom_timestamp_is_used
- Developer.cache_versioning = true
+ with_cache_versioning do
+ developer = Developer.first
+ first_key = developer.cache_key_with_version
- developer = Developer.first
- first_key = developer.cache_key_with_version
+ travel 10.seconds do
+ developer.touch
+ end
- travel 10.seconds do
- developer.touch
- end
+ second_key = developer.cache_key_with_version
- second_key = developer.cache_key_with_version
+ assert_not_equal first_key, second_key
+ end
+ end
- assert_not_equal first_key, second_key
+ def with_cache_versioning(value = true)
+ @old_cache_versioning = ActiveRecord::Base.cache_versioning
+ ActiveRecord::Base.cache_versioning = value
+ yield
ensure
- Developer.cache_versioning = false
+ ActiveRecord::Base.cache_versioning = @old_cache_versioning
end
end
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index ebe0b0aa87..d68cc40107 100644
--- a/activerecord/test/cases/invertible_migration_test.rb
+++ b/activerecord/test/cases/invertible_migration_test.rb
@@ -22,6 +22,14 @@ module ActiveRecord
end
end
+ class InvertibleTransactionMigration < InvertibleMigration
+ def change
+ transaction do
+ super
+ end
+ end
+ end
+
class InvertibleRevertMigration < SilentMigration
def change
revert do
@@ -215,7 +223,7 @@ module ActiveRecord
migration = InvertibleMigration.new
migration.migrate :up
migration.migrate :down
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
end
def test_migrate_revert
@@ -223,11 +231,11 @@ module ActiveRecord
revert = InvertibleRevertMigration.new
migration.migrate :up
revert.migrate :up
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
revert.migrate :down
assert migration.connection.table_exists?("horses")
migration.migrate :down
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
end
def test_migrate_revert_by_part
@@ -241,12 +249,12 @@ module ActiveRecord
}
migration.migrate :up
assert_equal [:both, :up], received
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
assert migration.connection.table_exists?("new_horses")
migration.migrate :down
assert_equal [:both, :up, :both, :down], received
assert migration.connection.table_exists?("horses")
- assert !migration.connection.table_exists?("new_horses")
+ assert_not migration.connection.table_exists?("new_horses")
end
def test_migrate_revert_whole_migration
@@ -255,11 +263,11 @@ module ActiveRecord
revert = RevertWholeMigration.new(klass)
migration.migrate :up
revert.migrate :up
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
revert.migrate :down
assert migration.connection.table_exists?("horses")
migration.migrate :down
- assert !migration.connection.table_exists?("horses")
+ assert_not migration.connection.table_exists?("horses")
end
end
@@ -268,7 +276,15 @@ module ActiveRecord
revert.migrate :down
assert revert.connection.table_exists?("horses")
revert.migrate :up
- assert !revert.connection.table_exists?("horses")
+ assert_not revert.connection.table_exists?("horses")
+ end
+
+ def test_migrate_revert_transaction
+ migration = InvertibleTransactionMigration.new
+ migration.migrate :up
+ assert migration.connection.table_exists?("horses")
+ migration.migrate :down
+ assert_not migration.connection.table_exists?("horses")
end
def test_migrate_revert_change_column_default
@@ -292,6 +308,8 @@ module ActiveRecord
migration2 = DisableExtension1.new
migration3 = DisableExtension2.new
+ assert_equal true, Horse.connection.extension_available?("hstore")
+
migration1.migrate(:up)
migration2.migrate(:up)
assert_equal true, Horse.connection.extension_enabled?("hstore")
@@ -341,7 +359,7 @@ module ActiveRecord
def test_legacy_down
LegacyMigration.migrate :up
LegacyMigration.migrate :down
- assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist"
+ assert_not ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist"
end
def test_up
@@ -352,7 +370,7 @@ module ActiveRecord
def test_down
LegacyMigration.up
LegacyMigration.down
- assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist"
+ assert_not ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist"
end
def test_migrate_down_with_table_name_prefix
@@ -361,7 +379,7 @@ module ActiveRecord
migration = InvertibleMigration.new
migration.migrate(:up)
assert_nothing_raised { migration.migrate(:down) }
- assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist"
+ assert_not ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist"
ensure
ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = ""
end
@@ -383,7 +401,7 @@ module ActiveRecord
connection = ActiveRecord::Base.connection
assert connection.index_exists?(:horses, :content),
"index on content should exist"
- assert !connection.index_exists?(:horses, :content, name: "horses_index_named"),
+ assert_not connection.index_exists?(:horses, :content, name: "horses_index_named"),
"horses_index_named index should not exist"
end
end
@@ -402,7 +420,7 @@ module ActiveRecord
UpOnlyMigration.new.migrate(:down) # should be no error
connection = ActiveRecord::Base.connection
- assert !connection.column_exists?(:horses, :oldie)
+ assert_not connection.column_exists?(:horses, :oldie)
Horse.reset_column_information
end
end
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 8513edb0ab..33bd74e114 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -445,32 +445,38 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_equal 0, car.wheels_count
assert_equal 0, car.lock_version
- previously_car_updated_at = car.updated_at
- travel(2.second) do
+ previously_updated_at = car.updated_at
+ previously_wheels_owned_at = car.wheels_owned_at
+ travel(1.second) do
Wheel.create!(wheelable: car)
end
assert_equal 1, car.reload.wheels_count
- assert_not_equal previously_car_updated_at, car.updated_at
assert_equal 1, car.lock_version
+ assert_operator previously_updated_at, :<, car.updated_at
+ assert_operator previously_wheels_owned_at, :<, car.wheels_owned_at
- previously_car_updated_at = car.updated_at
- travel(1.day) do
+ previously_updated_at = car.updated_at
+ previously_wheels_owned_at = car.wheels_owned_at
+ travel(2.second) do
car.wheels.first.update(size: 42)
end
assert_equal 1, car.reload.wheels_count
- assert_not_equal previously_car_updated_at, car.updated_at
assert_equal 2, car.lock_version
+ assert_operator previously_updated_at, :<, car.updated_at
+ assert_operator previously_wheels_owned_at, :<, car.wheels_owned_at
- previously_car_updated_at = car.updated_at
- travel(2.second) do
+ previously_updated_at = car.updated_at
+ previously_wheels_owned_at = car.wheels_owned_at
+ travel(3.second) do
car.wheels.first.destroy!
end
assert_equal 0, car.reload.wheels_count
- assert_not_equal previously_car_updated_at, car.updated_at
assert_equal 3, car.lock_version
+ assert_operator previously_updated_at, :<, car.updated_at
+ assert_operator previously_wheels_owned_at, :<, car.wheels_owned_at
end
def test_polymorphic_destroy_with_dependencies_and_lock_version
diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb
index e2742ed33e..ae2597adc8 100644
--- a/activerecord/test/cases/log_subscriber_test.rb
+++ b/activerecord/test/cases/log_subscriber_test.rb
@@ -44,6 +44,7 @@ class LogSubscriberTest < ActiveRecord::TestCase
def setup
@old_logger = ActiveRecord::Base.logger
Developer.primary_key
+ ActiveRecord::Base.connection.materialize_transactions
super
ActiveRecord::LogSubscriber.attach_to(:active_record)
end
@@ -177,11 +178,25 @@ class LogSubscriberTest < ActiveRecord::TestCase
logger = TestDebugLogSubscriber.new
logger.sql(Event.new(0, sql: "hi mom!"))
+ assert_equal 2, @logger.logged(:debug).size
assert_match(/↳/, @logger.logged(:debug).last)
ensure
ActiveRecord::Base.verbose_query_logs = false
end
+ def test_verbose_query_with_ignored_callstack
+ ActiveRecord::Base.verbose_query_logs = true
+
+ logger = TestDebugLogSubscriber.new
+ def logger.extract_query_source_location(*); nil; end
+
+ logger.sql(Event.new(0, sql: "hi mom!"))
+ assert_equal 1, @logger.logged(:debug).size
+ assert_no_match(/↳/, @logger.logged(:debug).last)
+ ensure
+ ActiveRecord::Base.verbose_query_logs = false
+ end
+
def test_verbose_query_logs_disabled_by_default
logger = TestDebugLogSubscriber.new
logger.sql(Event.new(0, sql: "hi mom!"))
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index 1494027182..cc0587fa50 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -196,6 +196,17 @@ module ActiveRecord
assert_equal "you can't redefine the primary key column 'testing_id'. To define a custom primary key, pass { id: false } to create_table.", error.message
end
+ def test_create_table_raises_when_defining_existing_column
+ error = assert_raise(ArgumentError) do
+ connection.create_table :testings do |t|
+ t.column :testing_column, :string
+ t.column :testing_column, :integer
+ end
+ end
+
+ assert_equal "you can't define an already defined column 'testing_column'.", error.message
+ end
+
def test_create_table_with_timestamps_should_create_datetime_columns
connection.create_table table_name do |t|
t.timestamps
@@ -205,8 +216,8 @@ module ActiveRecord
created_at_column = created_columns.detect { |c| c.name == "created_at" }
updated_at_column = created_columns.detect { |c| c.name == "updated_at" }
- assert !created_at_column.null
- assert !updated_at_column.null
+ assert_not created_at_column.null
+ assert_not updated_at_column.null
end
def test_create_table_with_timestamps_should_create_datetime_columns_with_options
@@ -408,7 +419,7 @@ module ActiveRecord
end
connection.change_table :testings do |t|
assert t.column_exists?(:foo)
- assert !(t.column_exists?(:bar))
+ assert_not (t.column_exists?(:bar))
end
end
@@ -451,7 +462,11 @@ module ActiveRecord
end
def test_create_table_with_force_cascade_drops_dependent_objects
- skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" if current_adapter?(:Mysql2Adapter)
+ if current_adapter?(:Mysql2Adapter)
+ skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE"
+ elsif current_adapter?(:SQLite3Adapter)
+ skip "SQLite3 does not support DROP TABLE CASCADE syntax"
+ end
# can't re-create table referenced by foreign key
assert_raises(ActiveRecord::StatementInvalid) do
@connection.create_table :trains, force: true
diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb
index 034bf32165..c108d372d1 100644
--- a/activerecord/test/cases/migration/change_table_test.rb
+++ b/activerecord/test/cases/migration/change_table_test.rb
@@ -164,6 +164,14 @@ module ActiveRecord
end
end
+ def test_column_creates_column_with_index
+ with_change_table do |t|
+ @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {}]
+ @connection.expect :add_index, nil, [:delete_me, :bar, {}]
+ t.column :bar, :integer, index: true
+ end
+ end
+
def test_index_creates_index
with_change_table do |t|
@connection.expect :add_index, nil, [:delete_me, :bar, {}]
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
index 3022121f4c..b6064500ee 100644
--- a/activerecord/test/cases/migration/column_attributes_test.rb
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -176,11 +176,9 @@ module ActiveRecord
if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
def test_out_of_range_limit_should_raise
- assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, limit: 10 }
-
- unless current_adapter?(:PostgreSQLAdapter)
- assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :text, limit: 0xfffffffff }
- end
+ assert_raise(ArgumentError) { add_column :test_models, :integer_too_big, :integer, limit: 10 }
+ assert_raise(ArgumentError) { add_column :test_models, :text_too_big, :text, limit: 0xfffffffff }
+ assert_raise(ArgumentError) { add_column :test_models, :binary_too_big, :binary, limit: 0xfffffffff }
end
end
end
diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb
index cedd9c44e3..cce3461e18 100644
--- a/activerecord/test/cases/migration/columns_test.rb
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -109,18 +109,10 @@ module ActiveRecord
add_index "test_models", ["hat_style", "hat_size"], unique: true
rename_column "test_models", "hat_size", "size"
- if current_adapter? :OracleAdapter
- assert_equal ["i_test_models_hat_style_size"], connection.indexes("test_models").map(&:name)
- else
- assert_equal ["index_test_models_on_hat_style_and_size"], connection.indexes("test_models").map(&:name)
- end
+ assert_equal ["index_test_models_on_hat_style_and_size"], connection.indexes("test_models").map(&:name)
rename_column "test_models", "hat_style", "style"
- if current_adapter? :OracleAdapter
- assert_equal ["i_test_models_style_size"], connection.indexes("test_models").map(&:name)
- else
- assert_equal ["index_test_models_on_style_and_size"], connection.indexes("test_models").map(&:name)
- end
+ assert_equal ["index_test_models_on_style_and_size"], connection.indexes("test_models").map(&:name)
end
def test_rename_column_does_not_rename_custom_named_index
@@ -144,7 +136,7 @@ module ActiveRecord
def test_remove_column_with_multi_column_index
# MariaDB starting with 10.2.8
# Dropping a column that is part of a multi-column UNIQUE constraint is not permitted.
- skip if current_adapter?(:Mysql2Adapter) && connection.mariadb? && connection.version >= "10.2.8"
+ skip if current_adapter?(:Mysql2Adapter) && connection.mariadb? && connection.database_version >= "10.2.8"
add_column "test_models", :hat_size, :integer
add_column "test_models", :hat_style, :string, limit: 100
@@ -318,6 +310,17 @@ module ActiveRecord
ensure
connection.drop_table(:my_table) rescue nil
end
+
+ def test_add_column_without_column_name
+ e = assert_raise ArgumentError do
+ connection.create_table "my_table", force: true do |t|
+ t.timestamp
+ end
+ end
+ assert_equal "Missing column name(s) for timestamp", e.message
+ ensure
+ connection.drop_table :my_table, if_exists: true
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index 3a11bb081b..01f8628fc5 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -117,13 +117,13 @@ module ActiveRecord
end
def test_invert_create_table_with_options_and_block
- block = Proc.new {}
+ block = Proc.new { }
drop_table = @recorder.inverse_of :create_table, [:people_reminders, id: false], &block
assert_equal [:drop_table, [:people_reminders, id: false], block], drop_table
end
def test_invert_drop_table
- block = Proc.new {}
+ block = Proc.new { }
create_table = @recorder.inverse_of :drop_table, [:people_reminders, id: false], &block
assert_equal [:create_table, [:people_reminders, id: false], block], create_table
end
@@ -145,7 +145,7 @@ module ActiveRecord
end
def test_invert_drop_join_table
- block = Proc.new {}
+ block = Proc.new { }
create_join_table = @recorder.inverse_of :drop_join_table, [:musics, :artists, table_name: :catalog], &block
assert_equal [:create_join_table, [:musics, :artists, table_name: :catalog], block], create_join_table
end
@@ -329,11 +329,24 @@ module ActiveRecord
assert_equal [:add_foreign_key, [:dogs, :people, primary_key: "person_id"]], enable
end
+ def test_invert_remove_foreign_key_with_primary_key_and_to_table_in_options
+ enable = @recorder.inverse_of :remove_foreign_key, [:dogs, to_table: :people, primary_key: "uuid"]
+ assert_equal [:add_foreign_key, [:dogs, :people, primary_key: "uuid"]], enable
+ end
+
def test_invert_remove_foreign_key_with_on_delete_on_update
enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade]
assert_equal [:add_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade]], enable
end
+ def test_invert_remove_foreign_key_with_to_table_in_options
+ enable = @recorder.inverse_of :remove_foreign_key, [:dogs, to_table: :people]
+ assert_equal [:add_foreign_key, [:dogs, :people]], enable
+
+ enable = @recorder.inverse_of :remove_foreign_key, [:dogs, to_table: :people, column: :owner_id]
+ assert_equal [:add_foreign_key, [:dogs, :people, column: :owner_id]], enable
+ end
+
def test_invert_remove_foreign_key_is_irreversible_without_to_table
assert_raises ActiveRecord::IrreversibleMigration do
@recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"]
@@ -347,6 +360,16 @@ module ActiveRecord
@recorder.inverse_of :remove_foreign_key, [:dogs]
end
end
+
+ def test_invert_transaction_with_irreversible_inside_is_irreversible
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.revert do
+ @recorder.transaction do
+ @recorder.execute "some sql"
+ end
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb
index 69a50674af..5753bd7117 100644
--- a/activerecord/test/cases/migration/compatibility_test.rb
+++ b/activerecord/test/cases/migration/compatibility_test.rb
@@ -86,8 +86,8 @@ module ActiveRecord
ActiveRecord::Migrator.new(:up, [migration]).migrate
- assert connection.columns(:more_testings).find { |c| c.name == "created_at" }.null
- assert connection.columns(:more_testings).find { |c| c.name == "updated_at" }.null
+ assert connection.column_exists?(:more_testings, :created_at, null: true)
+ assert connection.column_exists?(:more_testings, :updated_at, null: true)
ensure
connection.drop_table :more_testings rescue nil
end
@@ -103,8 +103,25 @@ module ActiveRecord
ActiveRecord::Migrator.new(:up, [migration]).migrate
- assert connection.columns(:testings).find { |c| c.name == "created_at" }.null
- assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null
+ assert connection.column_exists?(:testings, :created_at, null: true)
+ assert connection.column_exists?(:testings, :updated_at, null: true)
+ end
+
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_timestamps_have_null_constraints_if_not_present_in_migration_of_change_table_with_bulk
+ migration = Class.new(ActiveRecord::Migration[4.2]) {
+ def migrate(x)
+ change_table :testings, bulk: true do |t|
+ t.timestamps
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:testings, :created_at, null: true)
+ assert connection.column_exists?(:testings, :updated_at, null: true)
+ end
end
def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table
@@ -116,8 +133,70 @@ module ActiveRecord
ActiveRecord::Migrator.new(:up, [migration]).migrate
- assert connection.columns(:testings).find { |c| c.name == "created_at" }.null
- assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null
+ assert connection.column_exists?(:testings, :created_at, null: true)
+ assert connection.column_exists?(:testings, :updated_at, null: true)
+ end
+
+ def test_timestamps_doesnt_set_precision_on_create_table
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ create_table :more_testings do |t|
+ t.timestamps
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:more_testings, :created_at, null: false, **precision_implicit_default)
+ assert connection.column_exists?(:more_testings, :updated_at, null: false, **precision_implicit_default)
+ ensure
+ connection.drop_table :more_testings rescue nil
+ end
+
+ def test_timestamps_doesnt_set_precision_on_change_table
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ change_table :testings do |t|
+ t.timestamps default: Time.now
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default)
+ assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default)
+ end
+
+ if ActiveRecord::Base.connection.supports_bulk_alter?
+ def test_timestamps_doesnt_set_precision_on_change_table_with_bulk
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ change_table :testings, bulk: true do |t|
+ t.timestamps
+ end
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default)
+ assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default)
+ end
+ end
+
+ def test_timestamps_doesnt_set_precision_on_add_timestamps
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def migrate(x)
+ add_timestamps :testings, default: Time.now
+ end
+ }.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert connection.column_exists?(:testings, :created_at, null: false, **precision_implicit_default)
+ assert connection.column_exists?(:testings, :updated_at, null: false, **precision_implicit_default)
end
def test_legacy_migrations_raises_exception_when_inherited
@@ -127,6 +206,20 @@ module ActiveRecord
assert_match(/LegacyMigration < ActiveRecord::Migration\[4\.2\]/, e.message)
end
+ def test_legacy_migrations_not_raise_exception_on_reverting_transaction
+ migration = Class.new(ActiveRecord::Migration[5.2]) {
+ def change
+ transaction do
+ execute "select 1"
+ end
+ end
+ }.new
+
+ assert_nothing_raised do
+ migration.migrate(:down)
+ end
+ end
+
if current_adapter?(:PostgreSQLAdapter)
class Testing < ActiveRecord::Base
end
@@ -145,6 +238,15 @@ module ActiveRecord
ActiveRecord::Base.clear_cache!
end
end
+
+ private
+ def precision_implicit_default
+ if current_adapter?(:Mysql2Adapter)
+ { presicion: 0 }
+ else
+ { presicion: nil }
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb
index 83fb4f9385..e0cbb29dcf 100644
--- a/activerecord/test/cases/migration/create_join_table_test.rb
+++ b/activerecord/test/cases/migration/create_join_table_test.rb
@@ -95,42 +95,42 @@ module ActiveRecord
connection.create_join_table :artists, :musics
connection.drop_join_table :artists, :musics
- assert !connection.table_exists?("artists_musics")
+ assert_not connection.table_exists?("artists_musics")
end
def test_drop_join_table_with_strings
connection.create_join_table :artists, :musics
connection.drop_join_table "artists", "musics"
- assert !connection.table_exists?("artists_musics")
+ assert_not connection.table_exists?("artists_musics")
end
def test_drop_join_table_with_the_proper_order
connection.create_join_table :videos, :musics
connection.drop_join_table :videos, :musics
- assert !connection.table_exists?("musics_videos")
+ assert_not connection.table_exists?("musics_videos")
end
def test_drop_join_table_with_the_table_name
connection.create_join_table :artists, :musics, table_name: :catalog
connection.drop_join_table :artists, :musics, table_name: :catalog
- assert !connection.table_exists?("catalog")
+ assert_not connection.table_exists?("catalog")
end
def test_drop_join_table_with_the_table_name_as_string
connection.create_join_table :artists, :musics, table_name: "catalog"
connection.drop_join_table :artists, :musics, table_name: "catalog"
- assert !connection.table_exists?("catalog")
+ assert_not connection.table_exists?("catalog")
end
def test_drop_join_table_with_column_options
connection.create_join_table :artists, :musics, column_options: { null: true }
connection.drop_join_table :artists, :musics, column_options: { null: true }
- assert !connection.table_exists?("artists_musics")
+ assert_not connection.table_exists?("artists_musics")
end
def test_create_and_drop_join_table_with_common_prefix
@@ -139,7 +139,7 @@ module ActiveRecord
assert connection.table_exists?("audio_artists_musics")
connection.drop_join_table "audio_artists", "audio_musics"
- assert !connection.table_exists?("audio_artists_musics"), "Should have dropped join table, but didn't"
+ assert_not connection.table_exists?("audio_artists_musics"), "Should have dropped join table, but didn't"
end
end
diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb
index 50f5696ad1..5f1057f093 100644
--- a/activerecord/test/cases/migration/foreign_key_test.rb
+++ b/activerecord/test/cases/migration/foreign_key_test.rb
@@ -3,7 +3,7 @@
require "cases/helper"
require "support/schema_dumping_helper"
-if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
+if ActiveRecord::Base.connection.supports_foreign_keys?
module ActiveRecord
class Migration
class ForeignKeyInCreateTest < ActiveRecord::TestCase
@@ -19,11 +19,152 @@ if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
assert_equal "fk_name", fk.name unless current_adapter?(:SQLite3Adapter)
end
end
+
+ class ForeignKeyChangeColumnTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ class Rocket < ActiveRecord::Base
+ has_many :astronauts
+ end
+
+ class Astronaut < ActiveRecord::Base
+ belongs_to :rocket
+ end
+
+ class CreateRocketsMigration < ActiveRecord::Migration::Current
+ def up
+ create_table :rockets do |t|
+ t.string :name
+ end
+
+ create_table :astronauts do |t|
+ t.string :name
+ t.references :rocket, foreign_key: true
+ end
+ end
+
+ def down
+ drop_table :astronauts, if_exists: true
+ drop_table :rockets, if_exists: true
+ end
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @migration = CreateRocketsMigration.new
+ silence_stream($stdout) { @migration.migrate(:up) }
+ Rocket.reset_table_name
+ Rocket.reset_column_information
+ Astronaut.reset_table_name
+ Astronaut.reset_column_information
+ end
+
+ def teardown
+ silence_stream($stdout) { @migration.migrate(:down) }
+ Rocket.reset_table_name
+ Rocket.reset_column_information
+ Astronaut.reset_table_name
+ Astronaut.reset_column_information
+ end
+
+ def test_change_column_of_parent_table
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
+
+ @connection.change_column_null Rocket.table_name, :name, false
+
+ foreign_keys = @connection.foreign_keys(Astronaut.table_name)
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "myrocket", Rocket.first.name
+ assert_equal Astronaut.table_name, fk.from_table
+ assert_equal Rocket.table_name, fk.to_table
+ end
+
+ def test_rename_column_of_child_table
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
+
+ @connection.rename_column Astronaut.table_name, :name, :astronaut_name
+
+ foreign_keys = @connection.foreign_keys(Astronaut.table_name)
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "myrocket", Rocket.first.name
+ assert_equal Astronaut.table_name, fk.from_table
+ assert_equal Rocket.table_name, fk.to_table
+ end
+
+ def test_rename_reference_column_of_child_table
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
+
+ @connection.rename_column Astronaut.table_name, :rocket_id, :new_rocket_id
+
+ foreign_keys = @connection.foreign_keys(Astronaut.table_name)
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_equal "myrocket", Rocket.first.name
+ assert_equal Astronaut.table_name, fk.from_table
+ assert_equal Rocket.table_name, fk.to_table
+ assert_equal "new_rocket_id", fk.options[:column]
+ end
+
+ def test_remove_reference_column_of_child_table
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
+
+ @connection.remove_column Astronaut.table_name, :rocket_id
+
+ assert_empty @connection.foreign_keys(Astronaut.table_name)
+ end
+
+ def test_remove_foreign_key_by_column
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
+
+ @connection.remove_foreign_key Astronaut.table_name, column: :rocket_id
+
+ assert_empty @connection.foreign_keys(Astronaut.table_name)
+ end
+
+ def test_remove_foreign_key_by_column_in_change_table
+ rocket = Rocket.create!(name: "myrocket")
+ rocket.astronauts << Astronaut.create!
+
+ @connection.change_table Astronaut.table_name do |t|
+ t.remove_foreign_key column: :rocket_id
+ end
+
+ assert_empty @connection.foreign_keys(Astronaut.table_name)
+ end
+ end
+
+ class ForeignKeyChangeColumnWithPrefixTest < ForeignKeyChangeColumnTest
+ setup do
+ ActiveRecord::Base.table_name_prefix = "p_"
+ end
+
+ teardown do
+ ActiveRecord::Base.table_name_prefix = nil
+ end
+ end
+
+ class ForeignKeyChangeColumnWithSuffixTest < ForeignKeyChangeColumnTest
+ setup do
+ ActiveRecord::Base.table_name_suffix = "_s"
+ end
+
+ teardown do
+ ActiveRecord::Base.table_name_suffix = nil
+ end
+ end
end
end
-end
-if ActiveRecord::Base.connection.supports_foreign_keys?
module ActiveRecord
class Migration
class ForeignKeyTest < ActiveRecord::TestCase
@@ -62,7 +203,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
assert_equal "fk_test_has_pk", fk.to_table
assert_equal "fk_id", fk.column
assert_equal "pk_id", fk.primary_key
- assert_equal "fk_name", fk.name
+ assert_equal "fk_name", fk.name unless current_adapter?(:SQLite3Adapter)
end
def test_add_foreign_key_inferes_column
@@ -76,7 +217,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
assert_equal "rockets", fk.to_table
assert_equal "rocket_id", fk.column
assert_equal "id", fk.primary_key
- assert_equal("fk_rails_78146ddd2e", fk.name)
+ assert_equal "fk_rails_78146ddd2e", fk.name unless current_adapter?(:SQLite3Adapter)
end
def test_add_foreign_key_with_column
@@ -90,7 +231,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
assert_equal "rockets", fk.to_table
assert_equal "rocket_id", fk.column
assert_equal "id", fk.primary_key
- assert_equal("fk_rails_78146ddd2e", fk.name)
+ assert_equal "fk_rails_78146ddd2e", fk.name unless current_adapter?(:SQLite3Adapter)
end
def test_add_foreign_key_with_non_standard_primary_key
@@ -109,7 +250,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
assert_equal "space_shuttles", fk.to_table
assert_equal "pk", fk.primary_key
ensure
- @connection.remove_foreign_key :astronauts, name: "custom_pk"
+ @connection.remove_foreign_key :astronauts, name: "custom_pk", to_table: "space_shuttles"
@connection.drop_table :space_shuttles
end
@@ -183,6 +324,8 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
def test_foreign_key_exists_by_name
+ skip if current_adapter?(:SQLite3Adapter)
+
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
assert @connection.foreign_key_exists?(:astronauts, name: "fancy_named_fk")
@@ -214,6 +357,8 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
def test_remove_foreign_key_by_name
+ skip if current_adapter?(:SQLite3Adapter)
+
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk"
assert_equal 1, @connection.foreign_keys("astronauts").size
@@ -222,9 +367,22 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
def test_remove_foreign_non_existing_foreign_key_raises
- assert_raises ArgumentError do
+ e = assert_raises ArgumentError do
@connection.remove_foreign_key :astronauts, :rockets
end
+ assert_equal "Table 'astronauts' has no foreign key for rockets", e.message
+ end
+
+ def test_remove_foreign_key_by_the_select_one_on_the_same_table
+ @connection.add_foreign_key :astronauts, :rockets
+ @connection.add_reference :astronauts, :myrocket, foreign_key: { to_table: :rockets }
+
+ assert_equal 2, @connection.foreign_keys("astronauts").size
+
+ @connection.remove_foreign_key :astronauts, :rockets, column: "myrocket_id"
+
+ assert_equal [["astronauts", "rockets", "rocket_id"]],
+ @connection.foreign_keys("astronauts").map { |fk| [fk.from_table, fk.to_table, fk.column] }
end
if ActiveRecord::Base.connection.supports_validate_constraints?
@@ -303,7 +461,11 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
def test_schema_dumping_with_options
output = dump_table_schema "fk_test_has_fk"
- assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output
+ if current_adapter?(:SQLite3Adapter)
+ assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id"$}, output
+ else
+ assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output
+ end
end
def test_schema_dumping_with_custom_fk_ignore_pattern
@@ -357,7 +519,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
class CreateSchoolsAndClassesMigration < ActiveRecord::Migration::Current
- def change
+ def up
create_table(:schools)
create_table(:classes) do |t|
@@ -365,6 +527,11 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
add_foreign_key :classes, :schools
end
+
+ def down
+ drop_table :classes, if_exists: true
+ drop_table :schools, if_exists: true
+ end
end
def test_add_foreign_key_with_prefix
@@ -389,30 +556,4 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
end
end
-else
- module ActiveRecord
- class Migration
- class NoForeignKeySupportTest < ActiveRecord::TestCase
- setup do
- @connection = ActiveRecord::Base.connection
- end
-
- def test_add_foreign_key_should_be_noop
- @connection.add_foreign_key :clubs, :categories
- end
-
- def test_remove_foreign_key_should_be_noop
- @connection.remove_foreign_key :clubs, :categories
- end
-
- unless current_adapter?(:SQLite3Adapter)
- def test_foreign_keys_should_raise_not_implemented
- assert_raises NotImplementedError do
- @connection.foreign_keys("clubs")
- end
- end
- end
- end
- end
- end
end
diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb
index b25c6d84bc..5e688efc2b 100644
--- a/activerecord/test/cases/migration/index_test.rb
+++ b/activerecord/test/cases/migration/index_test.rb
@@ -99,7 +99,7 @@ module ActiveRecord
connection.add_index :testings, :foo
assert connection.index_exists?(:testings, :foo)
- assert !connection.index_exists?(:testings, :bar)
+ assert_not connection.index_exists?(:testings, :bar)
end
def test_index_exists_on_multiple_columns
@@ -131,15 +131,18 @@ module ActiveRecord
assert connection.index_exists?(:testings, :foo)
assert connection.index_exists?(:testings, :foo, name: "custom_index_name")
- assert !connection.index_exists?(:testings, :foo, name: "other_index_name")
+ assert_not connection.index_exists?(:testings, :foo, name: "other_index_name")
end
def test_remove_named_index
- connection.add_index :testings, :foo, name: "custom_index_name"
+ connection.add_index :testings, :foo, name: "index_testings_on_custom_index_name"
assert connection.index_exists?(:testings, :foo)
+
+ assert_raise(ArgumentError) { connection.remove_index(:testings, "custom_index_name") }
+
connection.remove_index :testings, :foo
- assert !connection.index_exists?(:testings, :foo)
+ assert_not connection.index_exists?(:testings, :foo)
end
def test_add_index_attribute_length_limit
@@ -155,14 +158,11 @@ module ActiveRecord
connection.add_index("testings", ["last_name", "first_name"])
connection.remove_index("testings", column: ["last_name", "first_name"])
- # Oracle adapter cannot have specified index name larger than 30 characters
- # Oracle adapter is shortening index name when just column list is given
- unless current_adapter?(:OracleAdapter)
- connection.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", name: :index_testings_on_last_name_and_first_name)
- connection.add_index("testings", ["last_name", "first_name"])
- connection.remove_index("testings", "last_name_and_first_name")
- end
+ connection.add_index("testings", ["last_name", "first_name"])
+ connection.remove_index("testings", name: :index_testings_on_last_name_and_first_name)
+ connection.add_index("testings", ["last_name", "first_name"])
+ connection.remove_index("testings", "last_name_and_first_name")
+
connection.add_index("testings", ["last_name", "first_name"])
connection.remove_index("testings", ["last_name", "first_name"])
@@ -203,7 +203,7 @@ module ActiveRecord
assert connection.index_exists?("testings", "last_name")
connection.remove_index("testings", "last_name")
- assert !connection.index_exists?("testings", "last_name")
+ assert_not connection.index_exists?("testings", "last_name")
end
end
diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb
index dedb5ea502..119bfd372a 100644
--- a/activerecord/test/cases/migration/pending_migrations_test.rb
+++ b/activerecord/test/cases/migration/pending_migrations_test.rb
@@ -25,7 +25,7 @@ module ActiveRecord
ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true
assert_raises ActiveRecord::PendingMigrationError do
- CheckPending.new(Proc.new {}).call({})
+ CheckPending.new(Proc.new { }).call({})
end
end
@@ -34,7 +34,7 @@ module ActiveRecord
migrator = Base.connection.migration_context
capture(:stdout) { migrator.migrate }
- assert_nil CheckPending.new(Proc.new {}).call({})
+ assert_nil CheckPending.new(Proc.new { }).call({})
end
end
end
diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb
index 7a092103c7..90a50a5651 100644
--- a/activerecord/test/cases/migration/references_foreign_key_test.rb
+++ b/activerecord/test/cases/migration/references_foreign_key_test.rb
@@ -2,7 +2,7 @@
require "cases/helper"
-if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
+if ActiveRecord::Base.connection.supports_foreign_keys?
module ActiveRecord
class Migration
class ReferencesForeignKeyInCreateTest < ActiveRecord::TestCase
@@ -65,9 +65,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys_in_create?
end
end
end
-end
-if ActiveRecord::Base.connection.supports_foreign_keys?
module ActiveRecord
class Migration
class ReferencesForeignKeyTest < ActiveRecord::TestCase
@@ -152,35 +150,38 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
test "foreign key methods respect pluralize_table_names" do
- begin
- original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names
- ActiveRecord::Base.pluralize_table_names = false
- @connection.create_table :testing
- @connection.change_table :testing_parents do |t|
- t.references :testing, foreign_key: true
- end
+ original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names
+ ActiveRecord::Base.pluralize_table_names = false
+ @connection.create_table :testing
+ @connection.change_table :testing_parents do |t|
+ t.references :testing, foreign_key: true
+ end
- fk = @connection.foreign_keys("testing_parents").first
- assert_equal "testing_parents", fk.from_table
- assert_equal "testing", fk.to_table
+ fk = @connection.foreign_keys("testing_parents").first
+ assert_equal "testing_parents", fk.from_table
+ assert_equal "testing", fk.to_table
- assert_difference "@connection.foreign_keys('testing_parents').size", -1 do
- @connection.remove_reference :testing_parents, :testing, foreign_key: true
- end
- ensure
- ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names
- @connection.drop_table "testing", if_exists: true
+ assert_difference "@connection.foreign_keys('testing_parents').size", -1 do
+ @connection.remove_reference :testing_parents, :testing, foreign_key: true
end
+ ensure
+ ActiveRecord::Base.pluralize_table_names = original_pluralize_table_names
+ @connection.drop_table "testing", if_exists: true
end
class CreateDogsMigration < ActiveRecord::Migration::Current
- def change
+ def up
create_table :dog_owners
create_table :dogs do |t|
t.references :dog_owner, foreign_key: true
end
end
+
+ def down
+ drop_table :dogs, if_exists: true
+ drop_table :dog_owners, if_exists: true
+ end
end
def test_references_foreign_key_with_prefix
@@ -234,24 +235,4 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
end
end
-else
- class ReferencesWithoutForeignKeySupportTest < ActiveRecord::TestCase
- setup do
- @connection = ActiveRecord::Base.connection
- @connection.create_table(:testing_parents, force: true)
- end
-
- teardown do
- @connection.drop_table("testings", if_exists: true)
- @connection.drop_table("testing_parents", if_exists: true)
- end
-
- test "ignores foreign keys defined with the table" do
- @connection.create_table :testings do |t|
- t.references :testing_parent, foreign_key: true
- end
-
- assert_includes @connection.data_sources, "testings"
- end
- end
end
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index e3fd7d1a7b..8e8ed494d9 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -71,13 +71,10 @@ class MigrationTest < ActiveRecord::TestCase
ActiveRecord::Migration.verbose = @verbose_was
end
- def test_migrator_migrations_path_is_deprecated
- assert_deprecated do
- ActiveRecord::Migrator.migrations_path = "/whatever"
- end
- ensure
+ def test_passing_migrations_paths_to_assume_migrated_upto_version_is_deprecated
+ ActiveRecord::SchemaMigration.create_table
assert_deprecated do
- ActiveRecord::Migrator.migrations_path = "db/migrate"
+ ActiveRecord::Base.connection.assume_migrated_upto_version(0, [])
end
end
@@ -87,7 +84,6 @@ class MigrationTest < ActiveRecord::TestCase
def test_migrator_versions
migrations_path = MIGRATIONS_ROOT + "/valid"
- old_path = ActiveRecord::Migrator.migrations_paths
migrator = ActiveRecord::MigrationContext.new(migrations_path)
migrator.up
@@ -100,24 +96,18 @@ class MigrationTest < ActiveRecord::TestCase
ActiveRecord::SchemaMigration.create!(version: 3)
assert_equal true, migrator.needs_migration?
- ensure
- ActiveRecord::MigrationContext.new(old_path)
end
def test_migration_detection_without_schema_migration_table
ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true
migrations_path = MIGRATIONS_ROOT + "/valid"
- old_path = ActiveRecord::Migrator.migrations_paths
migrator = ActiveRecord::MigrationContext.new(migrations_path)
assert_equal true, migrator.needs_migration?
- ensure
- ActiveRecord::MigrationContext.new(old_path)
end
def test_any_migrations
- old_path = ActiveRecord::Migrator.migrations_paths
migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid")
assert_predicate migrator, :any_migrations?
@@ -125,8 +115,6 @@ class MigrationTest < ActiveRecord::TestCase
migrator_empty = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/empty")
assert_not_predicate migrator_empty, :any_migrations?
- ensure
- ActiveRecord::MigrationContext.new(old_path)
end
def test_migration_version
@@ -136,6 +124,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
@@ -262,21 +280,21 @@ class MigrationTest < ActiveRecord::TestCase
def test_instance_based_migration_up
migration = MockMigration.new
- assert !migration.went_up, "have not gone up"
- assert !migration.went_down, "have not gone down"
+ assert_not migration.went_up, "have not gone up"
+ assert_not migration.went_down, "have not gone down"
migration.migrate :up
assert migration.went_up, "have gone up"
- assert !migration.went_down, "have not gone down"
+ assert_not migration.went_down, "have not gone down"
end
def test_instance_based_migration_down
migration = MockMigration.new
- assert !migration.went_up, "have not gone up"
- assert !migration.went_down, "have not gone down"
+ assert_not migration.went_up, "have not gone up"
+ assert_not migration.went_down, "have not gone down"
migration.migrate :down
- assert !migration.went_up, "have gone up"
+ assert_not migration.went_up, "have gone up"
assert migration.went_down, "have not gone down"
end
@@ -367,6 +385,7 @@ class MigrationTest < ActiveRecord::TestCase
assert_equal "changed", ActiveRecord::SchemaMigration.table_name
ensure
ActiveRecord::Base.schema_migrations_table_name = original_schema_migrations_table_name
+ ActiveRecord::SchemaMigration.reset_table_name
Reminder.reset_table_name
end
@@ -387,13 +406,13 @@ class MigrationTest < ActiveRecord::TestCase
assert_equal "changed", ActiveRecord::InternalMetadata.table_name
ensure
ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name
+ ActiveRecord::InternalMetadata.reset_table_name
Reminder.reset_table_name
end
def test_internal_metadata_stores_environment
current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
migrations_path = MIGRATIONS_ROOT + "/valid"
- old_path = ActiveRecord::Migrator.migrations_paths
migrator = ActiveRecord::MigrationContext.new(migrations_path)
migrator.up
@@ -410,7 +429,6 @@ class MigrationTest < ActiveRecord::TestCase
migrator.up
assert_equal new_env, ActiveRecord::InternalMetadata[:environment]
ensure
- migrator = ActiveRecord::MigrationContext.new(old_path)
ENV["RAILS_ENV"] = original_rails_env
ENV["RACK_ENV"] = original_rack_env
migrator.up
@@ -422,16 +440,11 @@ class MigrationTest < ActiveRecord::TestCase
current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
migrations_path = MIGRATIONS_ROOT + "/valid"
- old_path = ActiveRecord::Migrator.migrations_paths
- current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
migrator = ActiveRecord::MigrationContext.new(migrations_path)
migrator.up
assert_equal current_env, ActiveRecord::InternalMetadata[:environment]
assert_equal "bar", ActiveRecord::InternalMetadata[:foo]
- ensure
- migrator = ActiveRecord::MigrationContext.new(old_path)
- migrator.up
end
def test_proper_table_name_on_migration
@@ -548,7 +561,7 @@ class MigrationTest < ActiveRecord::TestCase
end
assert Person.connection.column_exists?(:something, :foo)
assert_nothing_raised { Person.connection.remove_column :something, :foo, :bar }
- assert !Person.connection.column_exists?(:something, :foo)
+ assert_not Person.connection.column_exists?(:something, :foo)
assert Person.connection.column_exists?(:something, :name)
assert Person.connection.column_exists?(:something, :number)
ensure
@@ -556,69 +569,68 @@ class MigrationTest < ActiveRecord::TestCase
end
end
- if current_adapter? :OracleAdapter
- def test_create_table_with_custom_sequence_name
- # table name is 29 chars, the standard sequence name will
- # be 33 chars and should be shortened
- assert_nothing_raised do
- begin
- Person.connection.create_table :table_with_name_thats_just_ok do |t|
- t.column :foo, :string, null: false
- end
- ensure
- Person.connection.drop_table :table_with_name_thats_just_ok rescue nil
- end
- end
-
- # should be all good w/ a custom sequence name
- assert_nothing_raised do
- begin
- Person.connection.create_table :table_with_name_thats_just_ok,
- sequence_name: "suitably_short_seq" do |t|
- t.column :foo, :string, null: false
- end
-
- Person.connection.execute("select suitably_short_seq.nextval from dual")
-
- ensure
- Person.connection.drop_table :table_with_name_thats_just_ok,
- sequence_name: "suitably_short_seq" rescue nil
- end
- end
-
- # confirm the custom sequence got dropped
- assert_raise(ActiveRecord::StatementInvalid) do
- Person.connection.execute("select suitably_short_seq.nextval from dual")
+ def test_decimal_scale_without_precision_should_raise
+ e = assert_raise(ArgumentError) do
+ Person.connection.create_table :test_decimal_scales, force: true do |t|
+ t.decimal :scaleonly, scale: 10
end
end
+
+ assert_equal "Error adding decimal column: precision cannot be empty if scale is specified", e.message
+ ensure
+ Person.connection.drop_table :test_decimal_scales, if_exists: true
end
if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
def test_out_of_range_integer_limit_should_raise
- e = assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do
+ e = assert_raise(ArgumentError) do
Person.connection.create_table :test_integer_limits, force: true do |t|
t.column :bigone, :integer, limit: 10
end
end
- assert_match(/No integer type has byte size 10/, e.message)
+ assert_includes e.message, "No integer type has byte size 10"
ensure
Person.connection.drop_table :test_integer_limits, if_exists: true
end
- end
- if current_adapter?(:Mysql2Adapter)
def test_out_of_range_text_limit_should_raise
- e = assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do
+ e = assert_raise(ArgumentError) do
Person.connection.create_table :test_text_limits, force: true do |t|
t.text :bigtext, limit: 0xfffffffff
end
end
- assert_match(/No text type has byte length #{0xfffffffff}/, e.message)
+ assert_includes e.message, "No text type has byte size #{0xfffffffff}"
ensure
Person.connection.drop_table :test_text_limits, if_exists: true
end
+
+ def test_out_of_range_binary_limit_should_raise
+ e = assert_raise(ArgumentError) do
+ Person.connection.create_table :test_binary_limits, force: true do |t|
+ t.binary :bigbinary, limit: 0xfffffffff
+ end
+ end
+
+ assert_includes e.message, "No binary type has byte size #{0xfffffffff}"
+ ensure
+ Person.connection.drop_table :test_binary_limits, if_exists: true
+ end
+ end
+
+ if current_adapter?(:Mysql2Adapter)
+ def test_invalid_text_size_should_raise
+ e = assert_raise(ArgumentError) do
+ Person.connection.create_table :test_text_sizes, force: true do |t|
+ t.text :bigtext, size: 0xfffffffff
+ end
+ end
+
+ assert_equal "#{0xfffffffff} is invalid :size value. Only :tiny, :medium, and :long are allowed.", e.message
+ ensure
+ Person.connection.drop_table :test_text_sizes, if_exists: true
+ end
end
if ActiveRecord::Base.connection.supports_advisory_locks?
@@ -727,15 +739,13 @@ class MigrationTest < ActiveRecord::TestCase
test_terminated = Concurrent::CountDownLatch.new
other_process = Thread.new do
- begin
- conn = ActiveRecord::Base.connection_pool.checkout
- conn.get_advisory_lock(lock_id)
- thread_lock.count_down
- test_terminated.wait # hold the lock open until we tested everything
- ensure
- conn.release_advisory_lock(lock_id)
- ActiveRecord::Base.connection_pool.checkin(conn)
- end
+ conn = ActiveRecord::Base.connection_pool.checkout
+ conn.get_advisory_lock(lock_id)
+ thread_lock.count_down
+ test_terminated.wait # hold the lock open until we tested everything
+ ensure
+ conn.release_advisory_lock(lock_id)
+ ActiveRecord::Base.connection_pool.checkin(conn)
end
thread_lock.wait # wait until the 'other process' has the lock
@@ -793,12 +803,20 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
end
def test_adding_multiple_columns
- assert_queries(1) do
+ classname = ActiveRecord::Base.connection.class.name[/[^:]*$/]
+ expected_query_count = {
+ "Mysql2Adapter" => 1,
+ "PostgreSQLAdapter" => 2, # one for bulk change, one for comment
+ }.fetch(classname) {
+ raise "need an expected query count for #{classname}"
+ }
+
+ assert_queries(expected_query_count) do
with_bulk_change_table do |t|
t.column :name, :string
t.string :qualification, :experience
t.integer :age, default: 0
- t.date :birthdate
+ t.date :birthdate, comment: "This is a comment"
t.timestamps null: true
end
end
@@ -806,6 +824,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
assert_equal 8, columns.size
[:name, :qualification, :experience].each { |s| assert_equal :string, column(s).type }
assert_equal "0", column(:age).default
+ assert_equal "This is a comment", column(:birthdate).comment
end
def test_removing_columns
@@ -822,7 +841,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
end
end
- [:qualification, :experience].each { |c| assert ! column(c) }
+ [:qualification, :experience].each { |c| assert_not column(c) }
assert column(:qualification_experience)
end
@@ -852,7 +871,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
name_age_index = index(:index_delete_me_on_name_and_age)
assert_equal ["name", "age"].sort, name_age_index.columns.sort
- assert ! name_age_index.unique
+ assert_not name_age_index.unique
assert index(:awesome_username_index).unique
end
@@ -880,7 +899,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
end
end
- assert ! index(:index_delete_me_on_name)
+ assert_not index(:index_delete_me_on_name)
new_name_index = index(:new_name_index)
assert new_name_index.unique
@@ -892,7 +911,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
t.date :birthdate
end
- assert ! column(:name).default
+ assert_not column(:name).default
assert_equal :date, column(:birthdate).type
classname = ActiveRecord::Base.connection.class.name[/[^:]*$/]
@@ -1150,7 +1169,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase
def test_check_pending_with_stdlib_logger
old, ActiveRecord::Base.logger = ActiveRecord::Base.logger, ::Logger.new($stdout)
quietly do
- assert_nothing_raised { ActiveRecord::Migration::CheckPending.new(Proc.new {}).call({}) }
+ assert_nothing_raised { ActiveRecord::Migration::CheckPending.new(Proc.new { }).call({}) }
end
ensure
ActiveRecord::Base.logger = old
diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb
index 873455cf67..30e199f1c5 100644
--- a/activerecord/test/cases/migrator_test.rb
+++ b/activerecord/test/cases/migrator_test.rb
@@ -100,7 +100,6 @@ class MigratorTest < ActiveRecord::TestCase
def test_finds_migrations_in_subdirectories
migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid_with_subdirectories").migrations
-
[[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i|
assert_equal migrations[i].version, pair.first
assert_equal migrations[i].name, pair.last
diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb
index 060d555607..87455e4fcb 100644
--- a/activerecord/test/cases/modules_test.rb
+++ b/activerecord/test/cases/modules_test.rb
@@ -32,7 +32,7 @@ class ModulesTest < ActiveRecord::TestCase
def test_module_spanning_associations
firm = MyApplication::Business::Firm.first
- assert !firm.clients.empty?, "Firm should have clients"
+ assert_not firm.clients.empty?, "Firm should have clients"
assert_nil firm.class.table_name.match("::"), "Firm shouldn't have the module appear in its table name"
end
@@ -155,7 +155,7 @@ class ModulesTest < ActiveRecord::TestCase
ActiveRecord::Base.store_full_sti_class = true
collection = Shop::Collection.first
- assert !collection.products.empty?, "Collection should have products"
+ assert_not collection.products.empty?, "Collection should have products"
assert_nothing_raised { collection.destroy }
ensure
ActiveRecord::Base.store_full_sti_class = old
@@ -166,7 +166,7 @@ class ModulesTest < ActiveRecord::TestCase
ActiveRecord::Base.store_full_sti_class = true
product = Shop::Product.first
- assert !product.variants.empty?, "Product should have variants"
+ assert_not product.variants.empty?, "Product should have variants"
assert_nothing_raised { product.destroy }
ensure
ActiveRecord::Base.store_full_sti_class = old
diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb
index 192d2f5251..f11c441c65 100644
--- a/activerecord/test/cases/multiple_db_test.rb
+++ b/activerecord/test/cases/multiple_db_test.rb
@@ -106,14 +106,12 @@ class MultipleDbTest < ActiveRecord::TestCase
end
def test_associations_should_work_when_model_has_no_connection
- begin
- ActiveRecord::Base.remove_connection
- assert_nothing_raised do
- College.first.courses.first
- end
- ensure
- ActiveRecord::Base.establish_connection :arunit
+ ActiveRecord::Base.remove_connection
+ assert_nothing_raised do
+ College.first.courses.first
end
+ ensure
+ ActiveRecord::Base.establish_connection :arunit
end
end
end
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
index 1ed3a61bbb..bb1c1ea17d 100644
--- a/activerecord/test/cases/nested_attributes_test.rb
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -83,7 +83,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
def test_a_model_should_respond_to_underscore_destroy_and_return_if_it_is_marked_for_destruction
ship = Ship.create!(name: "Nights Dirty Lightning")
- assert !ship._destroy
+ assert_not ship._destroy
ship.mark_for_destruction
assert ship._destroy
end
@@ -217,6 +217,18 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
mean_pirate.parrot_attributes = { name: "James" }
assert_equal "James", mean_pirate.parrot.name
end
+
+ def test_should_not_create_duplicates_with_create_with
+ Man.accepts_nested_attributes_for(:interests)
+
+ assert_difference("Interest.count", 1) do
+ Man.create_with(
+ interests_attributes: [{ topic: "Pirate king" }]
+ ).find_or_create_by!(
+ name: "Monkey D. Luffy"
+ )
+ end
+ end
end
class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
@@ -669,7 +681,6 @@ module NestedAttributesOnACollectionAssociationTests
def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models
@child_1.stub(:id, "ABC1X") do
@child_2.stub(:id, "ABC2X") do
-
@pirate.attributes = {
association_getter => [
{ id: @child_1.id, name: "Grace OMalley" },
@@ -835,7 +846,7 @@ module NestedAttributesOnACollectionAssociationTests
man = Man.create(name: "John")
interest = man.interests.create(topic: "bar", zine_id: 0)
assert interest.save
- assert !man.update(interests_attributes: { id: interest.id, zine_id: "foo" })
+ assert_not man.update(interests_attributes: { id: interest.id, zine_id: "foo" })
end
end
@@ -1095,3 +1106,15 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR
assert_equal ["Ship name can't be blank"], part.errors.full_messages
end
end
+
+class TestNestedAttributesWithExtend < ActiveRecord::TestCase
+ setup do
+ Pirate.accepts_nested_attributes_for :treasures
+ end
+
+ def test_extend_affects_nested_attributes
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ pirate.treasures_attributes = [{ id: nil }]
+ assert_equal "from extension", pirate.treasures[0].name
+ end
+end
diff --git a/activerecord/test/cases/null_relation_test.rb b/activerecord/test/cases/null_relation_test.rb
index 17527568f8..ee96ea1af6 100644
--- a/activerecord/test/cases/null_relation_test.rb
+++ b/activerecord/test/cases/null_relation_test.rb
@@ -10,26 +10,27 @@ class NullRelationTest < ActiveRecord::TestCase
fixtures :posts, :comments
def test_none
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_equal [], Developer.none
assert_equal [], Developer.all.none
end
end
def test_none_chainable
- assert_no_queries(ignore_none: false) do
+ Developer.send(:load_schema)
+ assert_no_queries do
assert_equal [], Developer.none.where(name: "David")
end
end
def test_none_chainable_to_existing_scope_extension_method
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_equal 1, Topic.anonymous_extension.none.one
end
end
def test_none_chained_to_methods_firing_queries_straight_to_db
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_equal [], Developer.none.pluck(:id, :name)
assert_equal 0, Developer.none.delete_all
assert_equal 0, Developer.none.update_all(name: "David")
@@ -39,7 +40,7 @@ class NullRelationTest < ActiveRecord::TestCase
end
def test_null_relation_content_size_methods
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_equal 0, Developer.none.size
assert_equal 0, Developer.none.count
assert_equal true, Developer.none.empty?
@@ -61,7 +62,7 @@ class NullRelationTest < ActiveRecord::TestCase
[:count, :sum].each do |method|
define_method "test_null_relation_#{method}" do
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_equal 0, Comment.none.public_send(method, :id)
assert_equal Hash.new, Comment.none.group(:post_id).public_send(method, :id)
end
@@ -70,7 +71,7 @@ class NullRelationTest < ActiveRecord::TestCase
[:average, :minimum, :maximum].each do |method|
define_method "test_null_relation_#{method}" do
- assert_no_queries(ignore_none: false) do
+ assert_no_queries do
assert_nil Comment.none.public_send(method, :id)
assert_equal Hash.new, Comment.none.group(:post_id).public_send(method, :id)
end
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/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index e4a65a48ca..d5057ad381 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -13,90 +13,15 @@ require "models/developer"
require "models/computer"
require "models/project"
require "models/minimalistic"
-require "models/warehouse_thing"
require "models/parrot"
require "models/minivan"
-require "models/owner"
require "models/person"
-require "models/pet"
require "models/ship"
-require "models/toy"
require "models/admin"
require "models/admin/user"
-require "rexml/document"
class PersistenceTest < ActiveRecord::TestCase
- fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, "warehouse-things", :authors, :author_addresses, :categorizations, :categories, :posts, :minivans, :pets, :toys
-
- # Oracle UPDATE does not support ORDER BY
- unless current_adapter?(:OracleAdapter)
- def test_update_all_ignores_order_without_limit_from_association
- author = authors(:david)
- assert_nothing_raised do
- assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all([ "body = ?", "bulk update!" ])
- end
- end
-
- def test_update_all_doesnt_ignore_order
- assert_equal authors(:david).id + 1, authors(:mary).id # make sure there is going to be a duplicate PK error
- test_update_with_order_succeeds = lambda do |order|
- begin
- Author.order(order).update_all("id = id + 1")
- rescue ActiveRecord::ActiveRecordError
- false
- end
- end
-
- if test_update_with_order_succeeds.call("id DESC")
- assert !test_update_with_order_succeeds.call("id ASC") # test that this wasn't a fluke and using an incorrect order results in an exception
- else
- # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead
- assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\z/i) do
- test_update_with_order_succeeds.call("id DESC")
- end
- end
- end
-
- def test_update_all_with_order_and_limit_updates_subset_only
- author = authors(:david)
- limited_posts = author.posts_sorted_by_id_limited
- assert_equal 1, limited_posts.size
- assert_equal 2, limited_posts.limit(2).size
- assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ])
- assert_equal "bulk update!", posts(:welcome).body
- assert_not_equal "bulk update!", posts(:thinking).body
- end
-
- def test_update_all_with_order_and_limit_and_offset_updates_subset_only
- author = authors(:david)
- limited_posts = author.posts_sorted_by_id_limited.offset(1)
- assert_equal 1, limited_posts.size
- assert_equal 2, limited_posts.limit(2).size
- assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ])
- assert_equal "bulk update!", posts(:thinking).body
- assert_not_equal "bulk update!", posts(:welcome).body
- end
-
- def test_delete_all_with_order_and_limit_deletes_subset_only
- author = authors(:david)
- limited_posts = Post.where(author: author).order(:id).limit(1)
- assert_equal 1, limited_posts.size
- assert_equal 2, limited_posts.limit(2).size
- assert_equal 1, limited_posts.delete_all
- assert_raise(ActiveRecord::RecordNotFound) { posts(:welcome) }
- assert posts(:thinking)
- end
-
- def test_delete_all_with_order_and_limit_and_offset_deletes_subset_only
- author = authors(:david)
- limited_posts = Post.where(author: author).order(:id).limit(1).offset(1)
- assert_equal 1, limited_posts.size
- assert_equal 2, limited_posts.limit(2).size
- assert_equal 1, limited_posts.delete_all
- assert_raise(ActiveRecord::RecordNotFound) { posts(:thinking) }
- assert posts(:welcome)
- end
- end
+ fixtures :topics, :companies, :developers, :accounts, :minimalistics, :authors, :author_addresses, :posts, :minivans
def test_update_many
topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } }
@@ -128,6 +53,20 @@ class PersistenceTest < ActiveRecord::TestCase
assert_not_equal "2 updated", Topic.find(2).content
end
+ def test_class_level_update_without_ids
+ topics = Topic.all
+ assert_equal 5, topics.length
+ topics.each do |topic|
+ assert_not_equal "updated", topic.content
+ end
+
+ updated = Topic.update(content: "updated")
+ assert_equal 5, updated.length
+ updated.each do |topic|
+ assert_equal "updated", topic.content
+ end
+ end
+
def test_class_level_update_is_affected_by_scoping
topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } }
@@ -145,34 +84,6 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal Topic.count, Topic.delete_all
end
- def test_delete_all_with_joins_and_where_part_is_hash
- pets = Pet.joins(:toys).where(toys: { name: "Bone" })
-
- assert_equal true, pets.exists?
- assert_equal pets.count, pets.delete_all
- end
-
- def test_delete_all_with_joins_and_where_part_is_not_hash
- pets = Pet.joins(:toys).where("toys.name = ?", "Bone")
-
- assert_equal true, pets.exists?
- assert_equal pets.count, pets.delete_all
- end
-
- def test_delete_all_with_left_joins
- pets = Pet.left_joins(:toys).where(toys: { name: "Bone" })
-
- assert_equal true, pets.exists?
- assert_equal pets.count, pets.delete_all
- end
-
- def test_delete_all_with_includes
- pets = Pet.includes(:toys).where(toys: { name: "Bone" })
-
- assert_equal true, pets.exists?
- assert_equal pets.count, pets.delete_all
- end
-
def test_increment_attribute
assert_equal 50, accounts(:signals37).credit_limit
accounts(:signals37).increment! :credit_limit
@@ -206,24 +117,33 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal initial_credit + 2, a1.reload.credit_limit
end
- def test_increment_updates_timestamps
+ def test_increment_with_touch_updates_timestamps
topic = topics(:first)
- topic.update_columns(updated_at: 5.minutes.ago)
- previous_updated_at = topic.updated_at
- topic.increment!(:replies_count, touch: true)
- assert_operator previous_updated_at, :<, topic.reload.updated_at
+ assert_equal 1, topic.replies_count
+ previously_updated_at = topic.updated_at
+ travel(1.second) do
+ topic.increment!(:replies_count, touch: true)
+ end
+ assert_equal 2, topic.reload.replies_count
+ assert_operator previously_updated_at, :<, topic.updated_at
end
- def test_destroy_all
- conditions = "author_name = 'Mary'"
- topics_by_mary = Topic.all.merge!(where: conditions, order: "id").to_a
- assert_not_empty topics_by_mary
-
- assert_difference("Topic.count", -topics_by_mary.size) do
- destroyed = Topic.where(conditions).destroy_all.sort_by(&:id)
- assert_equal topics_by_mary, destroyed
- assert destroyed.all?(&:frozen?), "destroyed topics should be frozen"
+ def test_increment_with_touch_an_attribute_updates_timestamps
+ topic = topics(:first)
+ assert_equal 1, topic.replies_count
+ previously_updated_at = topic.updated_at
+ previously_written_on = topic.written_on
+ travel(1.second) do
+ topic.increment!(:replies_count, touch: :written_on)
end
+ assert_equal 2, topic.reload.replies_count
+ assert_operator previously_updated_at, :<, topic.updated_at
+ assert_operator previously_written_on, :<, topic.written_on
+ end
+
+ def test_increment_with_no_arg
+ topic = topics(:first)
+ assert_raises(ArgumentError) { topic.increment! }
end
def test_destroy_many
@@ -290,6 +210,17 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal "The First Topic", Topic.find(copy.id).title
end
+ def test_becomes_wont_break_mutation_tracking
+ topic = topics(:first)
+ reply = topic.becomes(Reply)
+
+ assert_equal 1, topic.id_in_database
+ assert_empty topic.attributes_in_database
+
+ assert_equal 1, reply.id_in_database
+ assert_empty reply.attributes_in_database
+ end
+
def test_becomes_includes_changed_attributes
company = Company.new(name: "37signals")
client = company.becomes(Client)
@@ -322,12 +253,28 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal 41, accounts(:signals37, :reload).credit_limit
end
- def test_decrement_updates_timestamps
+ def test_decrement_with_touch_updates_timestamps
+ topic = topics(:first)
+ assert_equal 1, topic.replies_count
+ previously_updated_at = topic.updated_at
+ travel(1.second) do
+ topic.decrement!(:replies_count, touch: true)
+ end
+ assert_equal 0, topic.reload.replies_count
+ assert_operator previously_updated_at, :<, topic.updated_at
+ end
+
+ def test_decrement_with_touch_an_attribute_updates_timestamps
topic = topics(:first)
- topic.update_columns(updated_at: 5.minutes.ago)
- previous_updated_at = topic.updated_at
- topic.decrement!(:replies_count, touch: true)
- assert_operator previous_updated_at, :<, topic.reload.updated_at
+ assert_equal 1, topic.replies_count
+ previously_updated_at = topic.updated_at
+ previously_written_on = topic.written_on
+ travel(1.second) do
+ topic.decrement!(:replies_count, touch: :written_on)
+ end
+ assert_equal 0, topic.reload.replies_count
+ assert_operator previously_updated_at, :<, topic.updated_at
+ assert_operator previously_written_on, :<, topic.written_on
end
def test_create
@@ -513,19 +460,17 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_update_attribute_does_not_run_sql_if_attribute_is_not_changed
- klass = Class.new(Topic) do
- def self.name; "Topic"; end
- end
- topic = klass.create(title: "Another New Topic")
- assert_queries(0) do
+ topic = Topic.create(title: "Another New Topic")
+ assert_no_queries do
assert topic.update_attribute(:title, "Another New Topic")
end
end
def test_update_does_not_run_sql_if_record_has_not_changed
topic = Topic.create(title: "Another New Topic")
- assert_queries(0) { assert topic.update(title: "Another New Topic") }
- assert_queries(0) { assert topic.update(title: "Another New Topic") }
+ assert_no_queries do
+ assert topic.update(title: "Another New Topic")
+ end
end
def test_delete
@@ -595,32 +540,6 @@ class PersistenceTest < ActiveRecord::TestCase
assert_nil Topic.find(2).last_read
end
- def test_update_all_with_joins
- pets = Pet.joins(:toys).where(toys: { name: "Bone" })
-
- assert_equal true, pets.exists?
- assert_equal pets.count, pets.update_all(name: "Bob")
- end
-
- def test_update_all_with_left_joins
- pets = Pet.left_joins(:toys).where(toys: { name: "Bone" })
-
- assert_equal true, pets.exists?
- assert_equal pets.count, pets.update_all(name: "Bob")
- end
-
- def test_update_all_with_includes
- pets = Pet.includes(:toys).where(toys: { name: "Bone" })
-
- assert_equal true, pets.exists?
- assert_equal pets.count, pets.update_all(name: "Bob")
- end
-
- def test_update_all_with_non_standard_table_name
- assert_equal 1, WarehouseThing.where(id: 1).update_all(["value = ?", 0])
- assert_equal 0, WarehouseThing.find(1).value
- end
-
def test_delete_new_record
client = Client.new(name: "37signals")
client.delete
@@ -692,8 +611,8 @@ class PersistenceTest < ActiveRecord::TestCase
t = Topic.first
t.update_attribute(:title, "super_title")
assert_equal "super_title", t.title
- assert !t.changed?, "topic should not have changed"
- assert !t.title_changed?, "title should not have changed"
+ assert_not t.changed?, "topic should not have changed"
+ assert_not t.title_changed?, "title should not have changed"
assert_nil t.title_change, "title change should be nil"
t.reload
@@ -1142,21 +1061,19 @@ class PersistenceTest < ActiveRecord::TestCase
ActiveRecord::Base.connection.disable_query_cache!
end
- class SaveTest < ActiveRecord::TestCase
- def test_save_touch_false
- pet = Pet.create!(
- name: "Bob",
- created_at: 1.day.ago,
- updated_at: 1.day.ago)
+ def test_save_touch_false
+ parrot = Parrot.create!(
+ name: "Bob",
+ created_at: 1.day.ago,
+ updated_at: 1.day.ago)
- created_at = pet.created_at
- updated_at = pet.updated_at
+ created_at = parrot.created_at
+ updated_at = parrot.updated_at
- pet.name = "Barb"
- pet.save!(touch: false)
- assert_equal pet.created_at, created_at
- assert_equal pet.updated_at, updated_at
- end
+ parrot.name = "Barb"
+ parrot.save!(touch: false)
+ assert_equal parrot.created_at, created_at
+ assert_equal parrot.updated_at, updated_at
end
def test_reset_column_information_resets_children
diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb
index fa7f759e51..080aeb0989 100644
--- a/activerecord/test/cases/pooled_connections_test.rb
+++ b/activerecord/test/cases/pooled_connections_test.rb
@@ -25,14 +25,12 @@ class PooledConnectionsTest < ActiveRecord::TestCase
@timed_out = 0
threads.times do
Thread.new do
- begin
- conn = ActiveRecord::Base.connection_pool.checkout
- sleep 0.1
- ActiveRecord::Base.connection_pool.checkin conn
- @connection_count += 1
- rescue ActiveRecord::ConnectionTimeoutError
- @timed_out += 1
- end
+ conn = ActiveRecord::Base.connection_pool.checkout
+ sleep 0.1
+ ActiveRecord::Base.connection_pool.checkin conn
+ @connection_count += 1
+ rescue ActiveRecord::ConnectionTimeoutError
+ @timed_out += 1
end.join
end
end
@@ -42,14 +40,12 @@ class PooledConnectionsTest < ActiveRecord::TestCase
@connection_count = 0
@timed_out = 0
loops.times do
- begin
- conn = ActiveRecord::Base.connection_pool.checkout
- ActiveRecord::Base.connection_pool.checkin conn
- @connection_count += 1
- ActiveRecord::Base.connection.data_sources
- rescue ActiveRecord::ConnectionTimeoutError
- @timed_out += 1
- end
+ conn = ActiveRecord::Base.connection_pool.checkout
+ ActiveRecord::Base.connection_pool.checkin conn
+ @connection_count += 1
+ ActiveRecord::Base.connection.data_sources
+ rescue ActiveRecord::ConnectionTimeoutError
+ @timed_out += 1
end
end
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index 60dac91ec9..4759d3b6b2 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -305,6 +305,7 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase
test "schema dump primary key includes type and options" do
schema = dump_table_schema "barcodes"
assert_match %r{create_table "barcodes", primary_key: "code", id: :string, limit: 42}, schema
+ assert_no_match %r{t\.index \["code"\]}, schema
end
if current_adapter?(:Mysql2Adapter) && subsecond_precision_supported?
@@ -353,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
@@ -375,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 07be046fb7..eb32b690aa 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -13,12 +13,13 @@ class QueryCacheTest < ActiveRecord::TestCase
fixtures :tasks, :topics, :categories, :posts, :categories_posts
class ShouldNotHaveExceptionsLogger < ActiveRecord::LogSubscriber
- attr_reader :logger
+ attr_reader :logger, :events
def initialize
super
@logger = ::Logger.new File::NULL
@exception = false
+ @events = []
end
def exception?
@@ -26,6 +27,7 @@ class QueryCacheTest < ActiveRecord::TestCase
end
def sql(event)
+ @events << event
super
rescue
@exception = true
@@ -53,88 +55,97 @@ class QueryCacheTest < ActiveRecord::TestCase
assert_cache :off
end
- private def with_temporary_connection_pool
- old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name)
- new_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new ActiveRecord::Base.connection_pool.spec
- ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = new_pool
+ def test_query_cache_is_applied_to_connections_in_all_handlers
+ ActiveRecord::Base.connection_handlers = {
+ writing: ActiveRecord::Base.default_connection_handler,
+ reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new
+ }
+
+ ActiveRecord::Base.connected_to(role: :reading) do
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"])
+ end
- yield
+ 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_handler.send(:owner_to_pool)["primary"] = old_pool
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
end
def test_query_cache_across_threads
with_temporary_connection_pool do
- begin
- if in_memory_db?
- # Separate connections to an in-memory database create an entirely new database,
- # with an empty schema etc, so we just stub out this schema on the fly.
- ActiveRecord::Base.connection_pool.with_connection do |connection|
- connection.create_table :tasks do |t|
- t.datetime :starting
- t.datetime :ending
- end
+ if in_memory_db?
+ # Separate connections to an in-memory database create an entirely new database,
+ # with an empty schema etc, so we just stub out this schema on the fly.
+ ActiveRecord::Base.connection_pool.with_connection do |connection|
+ connection.create_table :tasks do |t|
+ t.datetime :starting
+ t.datetime :ending
end
- ActiveRecord::FixtureSet.create_fixtures(self.class.fixture_path, ["tasks"], {}, ActiveRecord::Base)
end
+ ActiveRecord::FixtureSet.create_fixtures(self.class.fixture_path, ["tasks"], {}, ActiveRecord::Base)
+ end
- ActiveRecord::Base.connection_pool.connections.each do |conn|
- assert_cache :off, conn
- end
+ ActiveRecord::Base.connection_pool.connections.each do |conn|
+ assert_cache :off, conn
+ end
- assert_not_predicate ActiveRecord::Base.connection, :nil?
- assert_cache :off
+ assert_not_predicate ActiveRecord::Base.connection, :nil?
+ assert_cache :off
- middleware {
- assert_cache :clean
+ middleware {
+ assert_cache :clean
- Task.find 1
- assert_cache :dirty
+ Task.find 1
+ assert_cache :dirty
- thread_1_connection = ActiveRecord::Base.connection
- ActiveRecord::Base.clear_active_connections!
- assert_cache :off, thread_1_connection
+ thread_1_connection = ActiveRecord::Base.connection
+ ActiveRecord::Base.clear_active_connections!
+ assert_cache :off, thread_1_connection
- started = Concurrent::Event.new
- checked = Concurrent::Event.new
+ started = Concurrent::Event.new
+ checked = Concurrent::Event.new
- thread_2_connection = nil
- thread = Thread.new {
- thread_2_connection = ActiveRecord::Base.connection
+ thread_2_connection = nil
+ thread = Thread.new {
+ thread_2_connection = ActiveRecord::Base.connection
- assert_equal thread_2_connection, thread_1_connection
- assert_cache :off
+ assert_equal thread_2_connection, thread_1_connection
+ assert_cache :off
- middleware {
- assert_cache :clean
+ middleware {
+ assert_cache :clean
- Task.find 1
- assert_cache :dirty
+ Task.find 1
+ assert_cache :dirty
- started.set
- checked.wait
+ started.set
+ checked.wait
- ActiveRecord::Base.clear_active_connections!
- }.call({})
- }
+ ActiveRecord::Base.clear_active_connections!
+ }.call({})
+ }
- started.wait
+ started.wait
- thread_1_connection = ActiveRecord::Base.connection
- assert_not_equal thread_1_connection, thread_2_connection
- assert_cache :dirty, thread_2_connection
- checked.set
- thread.join
+ thread_1_connection = ActiveRecord::Base.connection
+ assert_not_equal thread_1_connection, thread_2_connection
+ assert_cache :dirty, thread_2_connection
+ checked.set
+ thread.join
- assert_cache :off, thread_2_connection
- }.call({})
+ assert_cache :off, thread_2_connection
+ }.call({})
- ActiveRecord::Base.connection_pool.connections.each do |conn|
- assert_cache :off, conn
- end
- ensure
- ActiveRecord::Base.connection_pool.disconnect!
+ ActiveRecord::Base.connection_pool.connections.each do |conn|
+ assert_cache :off, conn
end
+ ensure
+ ActiveRecord::Base.connection_pool.disconnect!
end
end
@@ -198,7 +209,7 @@ class QueryCacheTest < ActiveRecord::TestCase
Task.cache do
assert_queries(2) { Task.find(1); Task.find(2) }
end
- assert_queries(0) { Task.find(1); Task.find(1); Task.find(2) }
+ assert_no_queries { Task.find(1); Task.find(1); Task.find(2) }
end
end
@@ -265,6 +276,26 @@ class QueryCacheTest < ActiveRecord::TestCase
end
end
+ def test_cache_notifications_can_be_overridden
+ logger = ShouldNotHaveExceptionsLogger.new
+ subscriber = ActiveSupport::Notifications.subscribe "sql.active_record", logger
+
+ connection = ActiveRecord::Base.connection.dup
+
+ def connection.cache_notification_info(sql, name, binds)
+ super.merge(neat: true)
+ end
+
+ connection.cache do
+ connection.select_all "select 1"
+ connection.select_all "select 1"
+ end
+
+ assert_equal true, logger.events.last.payload[:neat]
+ ensure
+ ActiveSupport::Notifications.unsubscribe subscriber
+ end
+
def test_cache_does_not_raise_exceptions
logger = ShouldNotHaveExceptionsLogger.new
subscriber = ActiveSupport::Notifications.subscribe "sql.active_record", logger
@@ -283,7 +314,7 @@ class QueryCacheTest < ActiveRecord::TestCase
payload[:sql].downcase!
end
- assert_raises frozen_error_class do
+ assert_raises FrozenError do
ActiveRecord::Base.cache do
assert_queries(1) { Task.find(1); Task.find(1) }
end
@@ -341,12 +372,10 @@ class QueryCacheTest < ActiveRecord::TestCase
assert_not_predicate Task, :connected?
Task.cache do
- begin
- assert_queries(1) { Task.find(1); Task.find(1) }
- ensure
- ActiveRecord::Base.connection_handler.remove_connection(Task.connection_specification_name)
- Task.connection_specification_name = spec_name
- end
+ assert_queries(1) { Task.find(1); Task.find(1) }
+ ensure
+ ActiveRecord::Base.connection_handler.remove_connection(Task.connection_specification_name)
+ Task.connection_specification_name = spec_name
end
end
end
@@ -360,7 +389,7 @@ class QueryCacheTest < ActiveRecord::TestCase
end
# Check that if the same query is run again, no queries are executed
- assert_queries(0) do
+ assert_no_queries do
assert_equal 0, Post.where(title: "test").to_a.count
end
@@ -415,8 +444,9 @@ class QueryCacheTest < ActiveRecord::TestCase
# Clear places where type information is cached
Task.reset_column_information
Task.initialize_find_by_cache
+ Task.define_attribute_methods
- assert_queries(0) do
+ assert_no_queries do
Task.find(1)
end
end
@@ -460,7 +490,6 @@ class QueryCacheTest < ActiveRecord::TestCase
assert_not ActiveRecord::Base.connection.query_cache_enabled
}.join
}.call({})
-
end
end
@@ -473,7 +502,56 @@ class QueryCacheTest < ActiveRecord::TestCase
}.call({})
end
+ def test_clear_query_cache_is_called_on_all_connections
+ skip "with in memory db, reading role won't be able to see database on writing role" if in_memory_db?
+ with_temporary_connection_pool do
+ ActiveRecord::Base.connection_handlers = {
+ writing: ActiveRecord::Base.default_connection_handler,
+ reading: ActiveRecord::ConnectionAdapters::ConnectionHandler.new
+ }
+
+ ActiveRecord::Base.connected_to(role: :reading) do
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations["arunit"])
+ end
+
+ mw = middleware { |env|
+ ActiveRecord::Base.connected_to(role: :reading) do
+ @topic = Topic.first
+ end
+
+ assert @topic
+
+ ActiveRecord::Base.connected_to(role: :writing) do
+ @topic.title = "It doesn't have to be crazy at work"
+ @topic.save!
+ end
+
+ assert_equal "It doesn't have to be crazy at work", @topic.title
+
+ ActiveRecord::Base.connected_to(role: :reading) do
+ @topic = Topic.first
+ assert_equal "It doesn't have to be crazy at work", @topic.title
+ end
+ }
+
+ mw.call({})
+ end
+ ensure
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
+ end
+
private
+
+ def with_temporary_connection_pool
+ old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name)
+ new_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new ActiveRecord::Base.connection_pool.spec
+ ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = new_pool
+
+ yield
+ ensure
+ ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = old_pool
+ end
+
def middleware(&app)
executor = Class.new(ActiveSupport::Executor)
ActiveRecord::QueryCache.install_executor_hooks executor
@@ -483,14 +561,14 @@ class QueryCacheTest < ActiveRecord::TestCase
def assert_cache(state, connection = ActiveRecord::Base.connection)
case state
when :off
- assert !connection.query_cache_enabled, "cache should be off"
+ assert_not connection.query_cache_enabled, "cache should be off"
assert connection.query_cache.empty?, "cache should be empty"
when :clean
assert connection.query_cache_enabled, "cache should be on"
assert connection.query_cache.empty?, "cache should be empty"
when :dirty
assert connection.query_cache_enabled, "cache should be on"
- assert !connection.query_cache.empty?, "cache should be dirty"
+ assert_not connection.query_cache.empty?, "cache should be dirty"
else
raise "unknown state"
end
@@ -518,19 +596,19 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
def test_find
assert_called(Task.connection, :clear_query_cache) do
- assert !Task.connection.query_cache_enabled
+ assert_not Task.connection.query_cache_enabled
Task.cache do
assert Task.connection.query_cache_enabled
Task.find(1)
Task.uncached do
- assert !Task.connection.query_cache_enabled
+ assert_not Task.connection.query_cache_enabled
Task.find(1)
end
assert Task.connection.query_cache_enabled
end
- assert !Task.connection.query_cache_enabled
+ assert_not Task.connection.query_cache_enabled
end
end
diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb
index 92eb0c814f..723fccc8d9 100644
--- a/activerecord/test/cases/quoting_test.rb
+++ b/activerecord/test/cases/quoting_test.rb
@@ -71,8 +71,8 @@ module ActiveRecord
with_timezone_config default: :utc do
t = Time.now.change(usec: 0)
- expected = t.getutc.change(year: 2000, month: 1, day: 1)
- expected = expected.to_s(:db).sub("2000-01-01 ", "")
+ expected = t.change(year: 2000, month: 1, day: 1)
+ expected = expected.getutc.to_s(:db).slice(11..-1)
assert_equal expected, @quoter.quoted_time(t)
end
@@ -89,6 +89,32 @@ module ActiveRecord
end
end
+ def test_quoted_time_dst_utc
+ with_env_tz "America/New_York" do
+ with_timezone_config default: :utc do
+ t = Time.new(2000, 7, 1, 0, 0, 0, "+04:30")
+
+ expected = t.change(year: 2000, month: 1, day: 1)
+ expected = expected.getutc.to_s(:db).slice(11..-1)
+
+ assert_equal expected, @quoter.quoted_time(t)
+ end
+ end
+ end
+
+ def test_quoted_time_dst_local
+ with_env_tz "America/New_York" do
+ with_timezone_config default: :local do
+ t = Time.new(2000, 7, 1, 0, 0, 0, "+04:30")
+
+ expected = t.change(year: 2000, month: 1, day: 1)
+ expected = expected.getlocal.to_s(:db).slice(11..-1)
+
+ assert_equal expected, @quoter.quoted_time(t)
+ end
+ end
+ end
+
def test_quoted_time_crazy
with_timezone_config default: :asdfasdf do
t = Time.now.change(usec: 0)
diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb
index 383e43ed55..059fa76132 100644
--- a/activerecord/test/cases/readonly_test.rb
+++ b/activerecord/test/cases/readonly_test.rb
@@ -23,7 +23,7 @@ class ReadOnlyTest < ActiveRecord::TestCase
assert_nothing_raised do
dev.name = "Luscious forbidden fruit."
- assert !dev.save
+ assert_not dev.save
dev.name = "Forbidden."
end
@@ -38,8 +38,8 @@ class ReadOnlyTest < ActiveRecord::TestCase
end
def test_find_with_readonly_option
- Developer.all.each { |d| assert !d.readonly? }
- Developer.readonly(false).each { |d| assert !d.readonly? }
+ Developer.all.each { |d| assert_not d.readonly? }
+ Developer.readonly(false).each { |d| assert_not d.readonly? }
Developer.readonly(true).each { |d| assert d.readonly? }
Developer.readonly.each { |d| assert d.readonly? }
end
@@ -55,14 +55,14 @@ class ReadOnlyTest < ActiveRecord::TestCase
def test_has_many_find_readonly
post = Post.find(1)
assert_not_empty post.comments
- assert !post.comments.any?(&:readonly?)
- assert !post.comments.to_a.any?(&:readonly?)
+ assert_not post.comments.any?(&:readonly?)
+ assert_not post.comments.to_a.any?(&:readonly?)
assert post.comments.readonly(true).all?(&:readonly?)
end
def test_has_many_with_through_is_not_implicitly_marked_readonly
assert people = Post.find(1).people
- assert !people.any?(&:readonly?)
+ assert_not people.any?(&:readonly?)
end
def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_by_id
diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb
index 61cb0f130d..402ddcf05a 100644
--- a/activerecord/test/cases/reaper_test.rb
+++ b/activerecord/test/cases/reaper_test.rb
@@ -36,19 +36,19 @@ module ActiveRecord
# A reaper with nil time should never reap connections
def test_nil_time
fp = FakePool.new
- assert !fp.reaped
+ assert_not fp.reaped
reaper = ConnectionPool::Reaper.new(fp, nil)
reaper.run
- assert !fp.reaped
+ assert_not fp.reaped
end
def test_some_time
fp = FakePool.new
- assert !fp.reaped
+ assert_not fp.reaped
reaper = ConnectionPool::Reaper.new(fp, 0.0001)
reaper.run
- until fp.reaped
+ until fp.flushed
Thread.pass
end
assert fp.reaped
@@ -61,9 +61,9 @@ module ActiveRecord
def test_reaping_frequency_configuration
spec = ActiveRecord::Base.connection_pool.spec.dup
- spec.config[:reaping_frequency] = 100
+ spec.config[:reaping_frequency] = "10.01"
pool = ConnectionPool.new spec
- assert_equal 100, pool.reaper.frequency
+ assert_equal 10.01, pool.reaper.frequency
end
def test_connection_pool_starts_reaper
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index ed19192ad9..abadafbad4 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -75,7 +75,7 @@ class ReflectionTest < ActiveRecord::TestCase
def test_column_null_not_null
subscriber = Subscriber.first
assert subscriber.column_for_attribute("name").null
- assert !subscriber.column_for_attribute("nick").null
+ assert_not subscriber.column_for_attribute("nick").null
end
def test_human_name_for_column
diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb
index 3f3d41980c..085006c9a2 100644
--- a/activerecord/test/cases/relation/delegation_test.rb
+++ b/activerecord/test/cases/relation/delegation_test.rb
@@ -5,7 +5,7 @@ require "models/post"
require "models/comment"
module ActiveRecord
- module DelegationWhitelistTests
+ module DelegationTests
ARRAY_DELEGATES = [
:+, :-, :|, :&, :[], :shuffle,
:all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index,
@@ -21,25 +21,14 @@ module ActiveRecord
assert_respond_to target, method
end
end
- end
-
- module DeprecatedArelDelegationTests
- AREL_METHODS = [
- :with, :orders, :froms, :project, :projections, :taken, :constraints, :exists, :locked, :where_sql,
- :ast, :source, :join_sources, :to_dot, :create_insert, :create_true, :create_false
- ]
- def test_deprecate_arel_delegation
- AREL_METHODS.each do |method|
- assert_deprecated { target.public_send(method) }
- assert_deprecated { target.public_send(method) }
- end
+ def test_not_respond_to_arel_method
+ assert_not_respond_to target, :exists
end
end
class DelegationAssociationTest < ActiveRecord::TestCase
- include DelegationWhitelistTests
- include DeprecatedArelDelegationTests
+ include DelegationTests
def target
Post.new.comments
@@ -47,8 +36,7 @@ module ActiveRecord
end
class DelegationRelationTest < ActiveRecord::TestCase
- include DelegationWhitelistTests
- include DeprecatedArelDelegationTests
+ include DelegationTests
def target
Comment.all
@@ -56,26 +44,28 @@ module ActiveRecord
end
class QueryingMethodsDelegationTest < ActiveRecord::TestCase
- QUERYING_METHODS = [
- :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?,
- :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!,
- :first_or_create, :first_or_create!, :first_or_initialize,
- :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by,
- :find_by, :find_by!,
- :destroy_all, :delete_all, :update_all,
- :find_each, :find_in_batches, :in_batches,
- :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or,
- :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending,
- :having, :create_with, :distinct, :references, :none, :unscope, :merge,
- :count, :average, :minimum, :maximum, :sum, :calculate,
- :pluck, :pick, :ids,
- ]
+ QUERYING_METHODS =
+ ActiveRecord::Batches.public_instance_methods(false) +
+ ActiveRecord::Calculations.public_instance_methods(false) +
+ ActiveRecord::FinderMethods.public_instance_methods(false) - [:raise_record_not_found_exception!] +
+ ActiveRecord::SpawnMethods.public_instance_methods(false) - [:spawn, :merge!] +
+ ActiveRecord::QueryMethods.public_instance_methods(false).reject { |method|
+ method.to_s.end_with?("=", "!", "value", "values", "clause")
+ } - [:reverse_order, :arel, :extensions, :construct_join_dependency] + [
+ :any?, :many?, :none?, :one?,
+ :first_or_create, :first_or_create!, :first_or_initialize,
+ :find_or_create_by, :find_or_create_by!, :find_or_initialize_by,
+ :create_or_find_by, :create_or_find_by!,
+ :destroy_all, :delete_all, :update_all, :touch_all, :delete_by, :destroy_by
+ ]
def test_delegate_querying_methods
klass = Class.new(ActiveRecord::Base) do
self.table_name = "posts"
end
+ assert_equal QUERYING_METHODS.sort, ActiveRecord::Querying::QUERYING_METHODS.sort
+
QUERYING_METHODS.each do |method|
assert_respond_to klass.all, method
assert_respond_to klass, method
diff --git a/activerecord/test/cases/relation/delete_all_test.rb b/activerecord/test/cases/relation/delete_all_test.rb
new file mode 100644
index 0000000000..d1c13fa1b5
--- /dev/null
+++ b/activerecord/test/cases/relation/delete_all_test.rb
@@ -0,0 +1,102 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/author"
+require "models/post"
+require "models/pet"
+require "models/toy"
+
+class DeleteAllTest < ActiveRecord::TestCase
+ fixtures :authors, :author_addresses, :posts, :pets, :toys
+
+ def test_destroy_all
+ davids = Author.where(name: "David")
+
+ # Force load
+ assert_equal [authors(:david)], davids.to_a
+ assert_predicate davids, :loaded?
+
+ assert_difference("Author.count", -1) do
+ destroyed = davids.destroy_all
+ assert_equal [authors(:david)], destroyed
+ assert_predicate destroyed.first, :frozen?
+ end
+
+ assert_equal [], davids.to_a
+ assert_predicate davids, :loaded?
+ end
+
+ def test_delete_all
+ davids = Author.where(name: "David")
+
+ assert_difference("Author.count", -1) { davids.delete_all }
+ assert_not_predicate davids, :loaded?
+ end
+
+ def test_delete_all_loaded
+ davids = Author.where(name: "David")
+
+ # Force load
+ assert_equal [authors(:david)], davids.to_a
+ assert_predicate davids, :loaded?
+
+ assert_difference("Author.count", -1) { davids.delete_all }
+
+ assert_equal [], davids.to_a
+ assert_predicate davids, :loaded?
+ end
+
+ def test_delete_all_with_unpermitted_relation_raises_error
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all }
+ assert_raises(ActiveRecord::ActiveRecordError) { Author.having("SUM(id) < 3").delete_all }
+ end
+
+ def test_delete_all_with_joins_and_where_part_is_hash
+ pets = Pet.joins(:toys).where(toys: { name: "Bone" })
+
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.delete_all
+ end
+
+ def test_delete_all_with_joins_and_where_part_is_not_hash
+ pets = Pet.joins(:toys).where("toys.name = ?", "Bone")
+
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.delete_all
+ end
+
+ def test_delete_all_with_left_joins
+ pets = Pet.left_joins(:toys).where(toys: { name: "Bone" })
+
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.delete_all
+ end
+
+ def test_delete_all_with_includes
+ pets = Pet.includes(:toys).where(toys: { name: "Bone" })
+
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.delete_all
+ end
+
+ def test_delete_all_with_order_and_limit_deletes_subset_only
+ author = authors(:david)
+ limited_posts = Post.where(author: author).order(:id).limit(1)
+ assert_equal 1, limited_posts.size
+ assert_equal 2, limited_posts.limit(2).size
+ assert_equal 1, limited_posts.delete_all
+ assert_raise(ActiveRecord::RecordNotFound) { posts(:welcome) }
+ assert posts(:thinking)
+ end
+
+ def test_delete_all_with_order_and_limit_and_offset_deletes_subset_only
+ author = authors(:david)
+ limited_posts = Post.where(author: author).order(:id).limit(1).offset(1)
+ assert_equal 1, limited_posts.size
+ assert_equal 2, limited_posts.limit(2).size
+ assert_equal 1, limited_posts.delete_all
+ assert_raise(ActiveRecord::RecordNotFound) { posts(:thinking) }
+ assert posts(:welcome)
+ end
+end
diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb
index 074ce9454f..5c5e760e34 100644
--- a/activerecord/test/cases/relation/merging_test.rb
+++ b/activerecord/test/cases/relation/merging_test.rb
@@ -78,6 +78,10 @@ class RelationMergingTest < ActiveRecord::TestCase
assert_equal 1, comments.count
end
+ def test_relation_merging_with_skip_query_cache
+ assert_equal Post.all.merge(Post.all.skip_query_cache!).skip_query_cache_value, true
+ end
+
def test_relation_merging_with_association
assert_queries(2) do # one for loading post, and another one merged query
post = Post.where(body: "Such a lovely day").first
@@ -117,6 +121,32 @@ class RelationMergingTest < ActiveRecord::TestCase
relation = relation.merge(Post.from("posts"))
assert_not_empty relation.from_clause
end
+
+ def test_merging_with_from_clause_on_different_class
+ assert Comment.joins(:post).merge(Post.from("posts")).first
+ end
+
+ def test_merging_with_order_with_binds
+ relation = Post.all.merge(Post.order([Arel.sql("title LIKE ?"), "%suffix"]))
+ assert_equal ["title LIKE '%suffix'"], relation.order_values
+ end
+
+ def test_merging_with_order_without_binds
+ relation = Post.all.merge(Post.order(Arel.sql("title LIKE '%?'")))
+ assert_equal ["title LIKE '%?'"], relation.order_values
+ end
+
+ def test_merging_annotations_respects_merge_order
+ assert_sql(%r{/\* foo \*/ /\* bar \*/}) do
+ Post.annotate("foo").merge(Post.annotate("bar")).first
+ end
+ assert_sql(%r{/\* bar \*/ /\* foo \*/}) do
+ Post.annotate("bar").merge(Post.annotate("foo")).first
+ end
+ assert_sql(%r{/\* foo \*/ /\* bar \*/ /\* baz \*/ /\* qux \*/}) do
+ Post.annotate("foo").annotate("bar").merge(Post.annotate("baz").annotate("qux")).first
+ end
+ end
end
class MergingDifferentRelationsTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb
index f82ecd4449..96249b8d51 100644
--- a/activerecord/test/cases/relation/mutation_test.rb
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -26,7 +26,7 @@ module ActiveRecord
assert relation.order!(:name).equal?(relation)
node = relation.order_values.first
assert_predicate node, :ascending?
- assert_equal :name, node.expr.name
+ assert_equal "name", node.expr.name
assert_equal "posts", node.expr.relation.name
end
@@ -89,7 +89,7 @@ module ActiveRecord
node = relation.order_values.first
assert_predicate node, :ascending?
- assert_equal :name, node.expr.name
+ assert_equal "name", node.expr.name
assert_equal "posts", node.expr.relation.name
end
diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb
index 7e418f9c7d..8623867864 100644
--- a/activerecord/test/cases/relation/or_test.rb
+++ b/activerecord/test/cases/relation/or_test.rb
@@ -30,6 +30,11 @@ module ActiveRecord
assert_equal expected, Post.where("id = 1").or(Post.none).to_a
end
+ def test_or_with_large_number
+ expected = Post.where("id = 1 or id = 9223372036854775808").to_a
+ assert_equal expected, Post.where(id: 1).or(Post.where(id: 9223372036854775808)).to_a
+ end
+
def test_or_with_bind_params
assert_equal Post.find([1, 2]).sort_by(&:id), Post.where(id: 1).or(Post.where(id: 2)).sort_by(&:id)
end
@@ -126,5 +131,12 @@ module ActiveRecord
expected = Author.find(1).posts + Post.where(title: "I don't have any comments")
assert_equal expected.sort_by(&:id), actual.sort_by(&:id)
end
+
+ def test_or_with_scope_on_association
+ author = Author.first
+ assert_nothing_raised do
+ author.top_posts.or(author.other_top_posts)
+ end
+ end
end
end
diff --git a/activerecord/test/cases/relation/select_test.rb b/activerecord/test/cases/relation/select_test.rb
index 0577e6bfdb..586aaadd0a 100644
--- a/activerecord/test/cases/relation/select_test.rb
+++ b/activerecord/test/cases/relation/select_test.rb
@@ -7,9 +7,21 @@ module ActiveRecord
class SelectTest < ActiveRecord::TestCase
fixtures :posts
- def test_select_with_nil_agrument
+ def test_select_with_nil_argument
expected = Post.select(:title).to_sql
assert_equal expected, Post.select(nil).select(:title).to_sql
end
+
+ def test_reselect
+ expected = Post.select(:title).to_sql
+ assert_equal expected, Post.select(:title, :body).reselect(:title).to_sql
+ end
+
+ def test_reselect_with_default_scope_select
+ expected = Post.select(:title).to_sql
+ actual = PostWithDefaultSelect.reselect(:title).to_sql
+
+ assert_equal expected, actual
+ end
end
end
diff --git a/activerecord/test/cases/relation/update_all_test.rb b/activerecord/test/cases/relation/update_all_test.rb
new file mode 100644
index 0000000000..e45531b4a9
--- /dev/null
+++ b/activerecord/test/cases/relation/update_all_test.rb
@@ -0,0 +1,324 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/author"
+require "models/category"
+require "models/comment"
+require "models/computer"
+require "models/developer"
+require "models/post"
+require "models/person"
+require "models/pet"
+require "models/toy"
+require "models/topic"
+require "models/tag"
+require "models/tagging"
+require "models/warehouse_thing"
+
+class UpdateAllTest < ActiveRecord::TestCase
+ fixtures :authors, :author_addresses, :comments, :developers, :posts, :people, :pets, :toys, :tags, :taggings, "warehouse-things"
+
+ class TopicWithCallbacks < ActiveRecord::Base
+ self.table_name = :topics
+ cattr_accessor :topic_count
+ before_update { |topic| topic.author_name = "David" if topic.author_name.blank? }
+ after_update { |topic| topic.class.topic_count = topic.class.count }
+ end
+
+ def test_update_all_with_scope
+ tag = Tag.first
+ Post.tagged_with(tag.id).update_all(title: "rofl")
+ posts = Post.tagged_with(tag.id).all.to_a
+ assert_operator posts.length, :>, 0
+ posts.each { |post| assert_equal "rofl", post.title }
+ end
+
+ def test_update_all_with_non_standard_table_name
+ assert_equal 1, WarehouseThing.where(id: 1).update_all(["value = ?", 0])
+ assert_equal 0, WarehouseThing.find(1).value
+ end
+
+ def test_update_all_with_blank_argument
+ assert_raises(ArgumentError) { Comment.update_all({}) }
+ end
+
+ def test_update_all_with_joins
+ pets = Pet.joins(:toys).where(toys: { name: "Bone" })
+
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.update_all(name: "Bob")
+ end
+
+ def test_update_all_with_left_joins
+ pets = Pet.left_joins(:toys).where(toys: { name: "Bone" })
+
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.update_all(name: "Bob")
+ end
+
+ def test_update_all_with_includes
+ pets = Pet.includes(:toys).where(toys: { name: "Bone" })
+
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.update_all(name: "Bob")
+ end
+
+ def test_update_all_with_joins_and_limit
+ comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).limit(1)
+ assert_equal 1, comments.update_all(post_id: posts(:thinking).id)
+ assert_equal posts(:thinking), comments(:greetings).post
+ end
+
+ def test_update_all_with_joins_and_limit_and_order
+ comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).order("comments.id").limit(1)
+ assert_equal 1, comments.update_all(post_id: posts(:thinking).id)
+ assert_equal posts(:thinking), comments(:greetings).post
+ assert_equal posts(:welcome), comments(:more_greetings).post
+ end
+
+ def test_update_all_with_joins_and_offset
+ all_comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id)
+ count = all_comments.count
+ comments = all_comments.offset(1)
+
+ assert_equal count - 1, comments.update_all(post_id: posts(:thinking).id)
+ end
+
+ def test_update_all_with_joins_and_offset_and_order
+ all_comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).order("posts.id", "comments.id")
+ count = all_comments.count
+ comments = all_comments.offset(1)
+
+ assert_equal count - 1, comments.update_all(post_id: posts(:thinking).id)
+ assert_equal posts(:thinking), comments(:more_greetings).post
+ assert_equal posts(:welcome), comments(:greetings).post
+ end
+
+ def test_update_counters_with_joins
+ assert_nil pets(:parrot).integer
+
+ Pet.joins(:toys).where(toys: { name: "Bone" }).update_counters(integer: 1)
+
+ assert_equal 1, pets(:parrot).reload.integer
+ end
+
+ def test_touch_all_updates_records_timestamps
+ david = developers(:david)
+ david_previously_updated_at = david.updated_at
+ jamis = developers(:jamis)
+ jamis_previously_updated_at = jamis.updated_at
+ Developer.where(name: "David").touch_all
+
+ assert_not_equal david_previously_updated_at, david.reload.updated_at
+ assert_equal jamis_previously_updated_at, jamis.reload.updated_at
+ end
+
+ def test_touch_all_with_custom_timestamp
+ developer = developers(:david)
+ previously_created_at = developer.created_at
+ previously_updated_at = developer.updated_at
+ Developer.where(name: "David").touch_all(:created_at)
+ developer.reload
+
+ assert_not_equal previously_created_at, developer.created_at
+ assert_not_equal previously_updated_at, developer.updated_at
+ end
+
+ def test_touch_all_with_given_time
+ developer = developers(:david)
+ previously_created_at = developer.created_at
+ previously_updated_at = developer.updated_at
+ new_time = Time.utc(2015, 2, 16, 4, 54, 0)
+ Developer.where(name: "David").touch_all(:created_at, time: new_time)
+ developer.reload
+
+ assert_not_equal previously_created_at, developer.created_at
+ assert_not_equal previously_updated_at, developer.updated_at
+ assert_equal new_time, developer.created_at
+ assert_equal new_time, developer.updated_at
+ end
+
+ def test_update_on_relation
+ topic1 = TopicWithCallbacks.create! title: "arel", author_name: nil
+ topic2 = TopicWithCallbacks.create! title: "activerecord", author_name: nil
+ topics = TopicWithCallbacks.where(id: [topic1.id, topic2.id])
+ topics.update(title: "adequaterecord")
+
+ assert_equal TopicWithCallbacks.count, TopicWithCallbacks.topic_count
+
+ assert_equal "adequaterecord", topic1.reload.title
+ assert_equal "adequaterecord", topic2.reload.title
+ # Testing that the before_update callbacks have run
+ assert_equal "David", topic1.reload.author_name
+ assert_equal "David", topic2.reload.author_name
+ end
+
+ def test_update_with_ids_on_relation
+ topic1 = TopicWithCallbacks.create!(title: "arel", author_name: nil)
+ topic2 = TopicWithCallbacks.create!(title: "activerecord", author_name: nil)
+ topics = TopicWithCallbacks.none
+ topics.update(
+ [topic1.id, topic2.id],
+ [{ title: "adequaterecord" }, { title: "adequaterecord" }]
+ )
+
+ assert_equal TopicWithCallbacks.count, TopicWithCallbacks.topic_count
+
+ assert_equal "adequaterecord", topic1.reload.title
+ assert_equal "adequaterecord", topic2.reload.title
+ # Testing that the before_update callbacks have run
+ assert_equal "David", topic1.reload.author_name
+ assert_equal "David", topic2.reload.author_name
+ end
+
+ def test_update_on_relation_passing_active_record_object_is_not_permitted
+ topic = Topic.create!(title: "Foo", author_name: nil)
+ assert_raises(ArgumentError) do
+ Topic.where(id: topic.id).update(topic, title: "Bar")
+ end
+ end
+
+ def test_update_all_cares_about_optimistic_locking
+ david = people(:david)
+
+ travel 5.seconds do
+ now = Time.now.utc
+ assert_not_equal now, david.updated_at
+
+ people = Person.where(id: people(:michael, :david, :susan))
+ expected = people.pluck(:lock_version)
+ expected.map! { |version| version + 1 }
+ people.update_all(updated_at: now)
+
+ assert_equal [now] * 3, people.pluck(:updated_at)
+ assert_equal expected, people.pluck(:lock_version)
+
+ assert_raises(ActiveRecord::StaleObjectError) do
+ david.touch(time: now)
+ end
+ end
+ end
+
+ def test_update_counters_cares_about_optimistic_locking
+ david = people(:david)
+
+ travel 5.seconds do
+ now = Time.now.utc
+ assert_not_equal now, david.updated_at
+
+ people = Person.where(id: people(:michael, :david, :susan))
+ expected = people.pluck(:lock_version)
+ expected.map! { |version| version + 1 }
+ people.update_counters(touch: [time: now])
+
+ assert_equal [now] * 3, people.pluck(:updated_at)
+ assert_equal expected, people.pluck(:lock_version)
+
+ assert_raises(ActiveRecord::StaleObjectError) do
+ david.touch(time: now)
+ end
+ end
+ end
+
+ def test_touch_all_cares_about_optimistic_locking
+ david = people(:david)
+
+ travel 5.seconds do
+ now = Time.now.utc
+ assert_not_equal now, david.updated_at
+
+ people = Person.where(id: people(:michael, :david, :susan))
+ expected = people.pluck(:lock_version)
+ expected.map! { |version| version + 1 }
+ people.touch_all(time: now)
+
+ assert_equal [now] * 3, people.pluck(:updated_at)
+ assert_equal expected, people.pluck(:lock_version)
+
+ assert_raises(ActiveRecord::StaleObjectError) do
+ david.touch(time: now)
+ end
+ end
+ end
+
+ def test_klass_level_update_all
+ travel 5.seconds do
+ now = Time.now.utc
+
+ Person.all.each do |person|
+ assert_not_equal now, person.updated_at
+ end
+
+ Person.update_all(updated_at: now)
+
+ Person.all.each do |person|
+ assert_equal now, person.updated_at
+ end
+ end
+ end
+
+ def test_klass_level_touch_all
+ travel 5.seconds do
+ now = Time.now.utc
+
+ Person.all.each do |person|
+ assert_not_equal now, person.updated_at
+ end
+
+ Person.touch_all(time: now)
+
+ Person.all.each do |person|
+ assert_equal now, person.updated_at
+ end
+ end
+ end
+
+ # Oracle UPDATE does not support ORDER BY
+ unless current_adapter?(:OracleAdapter)
+ def test_update_all_ignores_order_without_limit_from_association
+ author = authors(:david)
+ assert_nothing_raised do
+ assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all([ "body = ?", "bulk update!" ])
+ end
+ end
+
+ def test_update_all_doesnt_ignore_order
+ assert_equal authors(:david).id + 1, authors(:mary).id # make sure there is going to be a duplicate PK error
+ test_update_with_order_succeeds = lambda do |order|
+ Author.order(order).update_all("id = id + 1")
+ rescue ActiveRecord::ActiveRecordError
+ false
+ end
+
+ if test_update_with_order_succeeds.call("id DESC")
+ # test that this wasn't a fluke and using an incorrect order results in an exception
+ assert_not test_update_with_order_succeeds.call("id ASC")
+ else
+ # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead
+ assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\z/i) do
+ test_update_with_order_succeeds.call("id DESC")
+ end
+ end
+ end
+
+ def test_update_all_with_order_and_limit_updates_subset_only
+ author = authors(:david)
+ limited_posts = author.posts_sorted_by_id_limited
+ assert_equal 1, limited_posts.size
+ assert_equal 2, limited_posts.limit(2).size
+ assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ])
+ assert_equal "bulk update!", posts(:welcome).body
+ assert_not_equal "bulk update!", posts(:thinking).body
+ end
+
+ def test_update_all_with_order_and_limit_and_offset_updates_subset_only
+ author = authors(:david)
+ limited_posts = author.posts_sorted_by_id_limited.offset(1)
+ assert_equal 1, limited_posts.size
+ assert_equal 2, limited_posts.limit(2).size
+ assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ])
+ assert_equal "bulk update!", posts(:thinking).body
+ assert_not_equal "bulk update!", posts(:welcome).body
+ end
+ end
+end
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/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index 99797528b2..b045184d7d 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -14,6 +14,7 @@ require "models/price_estimate"
require "models/topic"
require "models/treasure"
require "models/vertex"
+require "support/stubs/strong_parameters"
module ActiveRecord
class WhereTest < ActiveRecord::TestCase
@@ -50,8 +51,13 @@ module ActiveRecord
assert_equal [chef], chefs.to_a
end
- def test_where_with_casted_value_is_nil
- assert_equal 4, Topic.where(last_read: "").count
+ def test_where_with_invalid_value
+ topics(:first).update!(parent_id: 0, written_on: nil, bonus_time: nil, last_read: nil)
+ assert_empty Topic.where(parent_id: Object.new)
+ assert_empty Topic.where(parent_id: "not-a-number")
+ assert_empty Topic.where(written_on: "")
+ assert_empty Topic.where(bonus_time: "")
+ assert_empty Topic.where(last_read: "")
end
def test_rewhere_on_root
@@ -334,31 +340,22 @@ module ActiveRecord
end
def test_where_with_strong_parameters
- protected_params = Class.new do
- attr_reader :permitted
- alias :permitted? :permitted
-
- def initialize(parameters)
- @parameters = parameters
- @permitted = false
- end
-
- def to_h
- @parameters
- end
-
- def permit!
- @permitted = true
- self
- end
- end
-
author = authors(:david)
- params = protected_params.new(name: author.name)
+ params = ProtectedParams.new(name: author.name)
assert_raises(ActiveModel::ForbiddenAttributesError) { Author.where(params) }
assert_equal author, Author.where(params.permit!).first
end
+ def test_where_with_large_number
+ assert_equal [authors(:bob)], Author.where(id: [3, 9223372036854775808])
+ assert_equal [authors(:bob)], Author.where(id: 3..9223372036854775808)
+ end
+
+ def test_to_sql_with_large_number
+ assert_equal [authors(:bob)], Author.find_by_sql(Author.where(id: [3, 9223372036854775808]).to_sql)
+ assert_equal [authors(:bob)], Author.find_by_sql(Author.where(id: 3..9223372036854775808).to_sql)
+ end
+
def test_where_with_unsupported_arguments
assert_raises(ArgumentError) { Author.where(42) }
end
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index 0f446e06aa..3f370e5ede 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -5,16 +5,17 @@ require "models/post"
require "models/comment"
require "models/author"
require "models/rating"
+require "models/categorization"
module ActiveRecord
class RelationTest < ActiveRecord::TestCase
- fixtures :posts, :comments, :authors, :author_addresses, :ratings
+ fixtures :posts, :comments, :authors, :author_addresses, :ratings, :categorizations
def test_construction
relation = Relation.new(FakeKlass, table: :b)
assert_equal FakeKlass, relation.klass
assert_equal :b, relation.table
- assert !relation.loaded, "relation is not loaded"
+ assert_not relation.loaded, "relation is not loaded"
end
def test_responds_to_model_and_returns_klass
@@ -100,6 +101,9 @@ module ActiveRecord
relation.merge!(relation)
assert_predicate relation, :empty_scope?
+
+ assert_not_predicate NullPost.all, :empty_scope?
+ assert_not_predicate FirstPost.all, :empty_scope?
end
def test_bad_constants_raise_errors
@@ -223,6 +227,30 @@ module ActiveRecord
assert_equal manual_comments_on_post_that_have_author.size, merged_authors_with_commented_posts_relation.to_a.size
end
+ def test_relation_merging_with_merged_symbol_joins_is_aliased
+ categorizations_with_authors = Categorization.joins(:author)
+ queries = capture_sql { Post.joins(:author, :categorizations).merge(Author.select(:id)).merge(categorizations_with_authors).to_a }
+
+ nb_inner_join = queries.sum { |sql| sql.scan(/INNER\s+JOIN/i).size }
+ assert_equal 3, nb_inner_join, "Wrong amount of INNER JOIN in query"
+
+ # using `\W` as the column separator
+ assert queries.any? { |sql| %r[INNER\s+JOIN\s+#{Regexp.escape(Author.quoted_table_name)}\s+\Wauthors_categorizations\W]i.match?(sql) }, "Should be aliasing the child INNER JOINs in query"
+ end
+
+ def test_relation_with_merged_joins_aliased_works
+ categorizations_with_authors = Categorization.joins(:author)
+ posts_with_joins_and_merges = Post.joins(:author, :categorizations)
+ .merge(Author.select(:id)).merge(categorizations_with_authors)
+
+ author_with_posts = Author.joins(:posts).ids
+ categorizations_with_author = Categorization.joins(:author).ids
+ posts_with_author_and_categorizations = Post.joins(:categorizations).where(author_id: author_with_posts, categorizations: { id: categorizations_with_author }).ids
+
+ assert_equal posts_with_author_and_categorizations.size, posts_with_joins_and_merges.count
+ assert_equal posts_with_author_and_categorizations.size, posts_with_joins_and_merges.to_a.size
+ end
+
def test_relation_merging_with_joins_as_join_dependency_pick_proper_parent
post = Post.create!(title: "haha", body: "huhu")
comment = post.comments.create!(body: "hu")
@@ -264,6 +292,7 @@ module ActiveRecord
klass.create!(description: "foo")
assert_equal ["foo"], klass.select(:description).from(klass.all).map(&:desc)
+ assert_equal ["foo"], klass.reselect(:description).from(klass.all).map(&:desc)
end
def test_relation_merging_with_merged_joins_as_strings
@@ -282,6 +311,58 @@ module ActiveRecord
assert_equal 3, ratings.count
end
+ def test_relation_with_annotation_includes_comment_in_to_sql
+ post_with_annotation = Post.where(id: 1).annotate("foo")
+ assert_match %r{= 1 /\* foo \*/}, post_with_annotation.to_sql
+ end
+
+ def test_relation_with_annotation_includes_comment_in_sql
+ post_with_annotation = Post.where(id: 1).annotate("foo")
+ assert_sql(%r{/\* foo \*/}) do
+ assert post_with_annotation.first, "record should be found"
+ end
+ end
+
+ def test_relation_with_annotation_chains_sql_comments
+ post_with_annotation = Post.where(id: 1).annotate("foo").annotate("bar")
+ assert_sql(%r{/\* foo \*/ /\* bar \*/}) do
+ assert post_with_annotation.first, "record should be found"
+ end
+ end
+
+ def test_relation_with_annotation_filters_sql_comment_delimiters
+ post_with_annotation = Post.where(id: 1).annotate("**//foo//**")
+ assert_match %r{= 1 /\* foo \*/}, post_with_annotation.to_sql
+ end
+
+ def test_relation_with_annotation_includes_comment_in_count_query
+ post_with_annotation = Post.annotate("foo")
+ all_count = Post.all.to_a.count
+ assert_sql(%r{/\* foo \*/}) do
+ assert_equal all_count, post_with_annotation.count
+ end
+ end
+
+ def test_relation_without_annotation_does_not_include_an_empty_comment
+ log = capture_sql do
+ Post.where(id: 1).first
+ end
+
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\*}) }, :empty?
+ end
+
+ def test_relation_with_optimizer_hints_filters_sql_comment_delimiters
+ post_with_hint = Post.where(id: 1).optimizer_hints("**//BADHINT//**")
+ assert_match %r{BADHINT}, post_with_hint.to_sql
+ assert_no_match %r{\*/BADHINT}, post_with_hint.to_sql
+ assert_no_match %r{\*//BADHINT}, post_with_hint.to_sql
+ assert_no_match %r{BADHINT/\*}, post_with_hint.to_sql
+ assert_no_match %r{BADHINT//\*}, post_with_hint.to_sql
+ post_with_hint = Post.where(id: 1).optimizer_hints("/*+ BADHINT */")
+ assert_match %r{/\*\+ BADHINT \*/}, post_with_hint.to_sql
+ end
+
class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value
def type
:string
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index cf65789b97..2417775ef1 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -9,9 +9,12 @@ require "models/comment"
require "models/author"
require "models/entrant"
require "models/developer"
+require "models/project"
+require "models/person"
require "models/computer"
require "models/reply"
require "models/company"
+require "models/contract"
require "models/bird"
require "models/car"
require "models/engine"
@@ -25,12 +28,7 @@ require "models/edge"
require "models/subscriber"
class RelationTest < ActiveRecord::TestCase
- fixtures :authors, :author_addresses, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :categories_posts, :posts, :comments, :tags, :taggings, :cars, :minivans
-
- class TopicWithCallbacks < ActiveRecord::Base
- self.table_name = :topics
- before_update { |topic| topic.author_name = "David" if topic.author_name.blank? }
- end
+ fixtures :authors, :author_addresses, :topics, :entrants, :developers, :people, :companies, :developers_projects, :accounts, :categories, :categorizations, :categories_posts, :posts, :comments, :tags, :taggings, :cars, :minivans
def test_do_not_double_quote_string_id
van = Minivan.last
@@ -184,15 +182,64 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ def test_select_with_from_includes_original_table_name
+ relation = Comment.joins(:post).select(:id).order(:id)
+ subquery = Comment.from("#{Comment.table_name} /*! USE INDEX (PRIMARY) */").joins(:post).select(:id).order(:id)
+ assert_equal relation.map(&:id), subquery.map(&:id)
+ end
+
+ def test_pluck_with_from_includes_original_table_name
+ relation = Comment.joins(:post).order(:id)
+ subquery = Comment.from("#{Comment.table_name} /*! USE INDEX (PRIMARY) */").joins(:post).order(:id)
+ assert_equal relation.pluck(:id), subquery.pluck(:id)
+ end
+
+ def test_select_with_from_includes_quoted_original_table_name
+ relation = Comment.joins(:post).select(:id).order(:id)
+ subquery = Comment.from("#{Comment.quoted_table_name} /*! USE INDEX (PRIMARY) */").joins(:post).select(:id).order(:id)
+ assert_equal relation.map(&:id), subquery.map(&:id)
+ end
+
+ def test_pluck_with_from_includes_quoted_original_table_name
+ relation = Comment.joins(:post).order(:id)
+ subquery = Comment.from("#{Comment.quoted_table_name} /*! USE INDEX (PRIMARY) */").joins(:post).order(:id)
+ assert_equal relation.pluck(:id), subquery.pluck(:id)
+ end
+
+ def test_select_with_subquery_in_from_uses_original_table_name
+ relation = Comment.joins(:post).select(:id).order(:id)
+ # Avoid subquery flattening by adding distinct to work with SQLite < 3.20.0.
+ subquery = Comment.from(Comment.all.distinct, Comment.quoted_table_name).joins(:post).select(:id).order(:id)
+ assert_equal relation.map(&:id), subquery.map(&:id)
+ end
+
+ def test_pluck_with_subquery_in_from_uses_original_table_name
+ relation = Comment.joins(:post).order(:id)
+ subquery = Comment.from(Comment.all, Comment.quoted_table_name).joins(:post).order(:id)
+ assert_equal relation.pluck(:id), subquery.pluck(:id)
+ end
+
def test_select_with_subquery_in_from_does_not_use_original_table_name
relation = Comment.group(:type).select("COUNT(post_id) AS post_count, type")
- subquery = Comment.from(relation).select("type", "post_count")
+ subquery = Comment.from(relation, "grouped_#{Comment.table_name}").select("type", "post_count")
assert_equal(relation.map(&:post_count).sort, subquery.map(&:post_count).sort)
end
def test_group_with_subquery_in_from_does_not_use_original_table_name
relation = Comment.group(:type).select("COUNT(post_id) AS post_count,type")
- subquery = Comment.from(relation).group("type").average("post_count")
+ subquery = Comment.from(relation, "grouped_#{Comment.table_name}").group("type").average("post_count")
+ assert_equal(relation.map(&:post_count).sort, subquery.values.sort)
+ end
+
+ def test_select_with_subquery_string_in_from_does_not_use_original_table_name
+ relation = Comment.group(:type).select("COUNT(post_id) AS post_count, type")
+ subquery = Comment.from("(#{relation.to_sql}) #{Comment.table_name}_grouped").select("type", "post_count")
+ assert_equal(relation.map(&:post_count).sort, subquery.map(&:post_count).sort)
+ end
+
+ def test_group_with_subquery_string_in_from_does_not_use_original_table_name
+ relation = Comment.group(:type).select("COUNT(post_id) AS post_count,type")
+ subquery = Comment.from("(#{relation.to_sql}) #{Comment.table_name}_grouped").group("type").average("post_count")
assert_equal(relation.map(&:post_count).sort, subquery.values.sort)
end
@@ -293,8 +340,17 @@ class RelationTest < ActiveRecord::TestCase
Topic.order(Arel.sql("title NULLS FIRST")).reverse_order
end
assert_raises(ActiveRecord::IrreversibleOrderError) do
+ Topic.order(Arel.sql("title NULLS FIRST")).reverse_order
+ end
+ assert_raises(ActiveRecord::IrreversibleOrderError) do
Topic.order(Arel.sql("title nulls last")).reverse_order
end
+ assert_raises(ActiveRecord::IrreversibleOrderError) do
+ Topic.order(Arel.sql("title NULLS FIRST, author_name")).reverse_order
+ end
+ assert_raises(ActiveRecord::IrreversibleOrderError) do
+ Topic.order(Arel.sql("author_name, title nulls last")).reverse_order
+ end
end
def test_default_reverse_order_on_table_without_primary_key
@@ -483,21 +539,6 @@ class RelationTest < ActiveRecord::TestCase
assert_nothing_raised { Topic.reorder([]) }
end
- def test_respond_to_delegates_to_arel
- relation = Topic.all
- fake_arel = Struct.new(:responds) {
- def respond_to?(method, access = false)
- responds << [method, access]
- end
- }.new []
-
- relation.extend(Module.new { attr_accessor :arel })
- relation.arel = fake_arel
-
- relation.respond_to?(:matching_attributes)
- assert_equal [:matching_attributes, false], fake_arel.responds.first
- end
-
def test_respond_to_dynamic_finders
relation = Topic.all
@@ -511,7 +552,7 @@ class RelationTest < ActiveRecord::TestCase
end
def test_find_with_readonly_option
- Developer.all.each { |d| assert !d.readonly? }
+ Developer.all.each { |d| assert_not d.readonly? }
Developer.all.readonly.each { |d| assert d.readonly? }
end
@@ -561,6 +602,13 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ def test_extracted_association
+ relation_authors = assert_queries(2) { Post.all.extract_associated(:author) }
+ root_authors = assert_queries(2) { Post.extract_associated(:author) }
+ assert_equal relation_authors, root_authors
+ assert_equal Post.all.collect(&:author), relation_authors
+ end
+
def test_find_with_included_associations
assert_queries(2) do
posts = Post.includes(:comments).order("posts.id")
@@ -859,45 +907,6 @@ class RelationTest < ActiveRecord::TestCase
assert_equal authors(:bob), authors.last
end
- def test_destroy_all
- davids = Author.where(name: "David")
-
- # Force load
- assert_equal [authors(:david)], davids.to_a
- assert_predicate davids, :loaded?
-
- assert_difference("Author.count", -1) { davids.destroy_all }
-
- assert_equal [], davids.to_a
- assert_predicate davids, :loaded?
- end
-
- def test_delete_all
- davids = Author.where(name: "David")
-
- assert_difference("Author.count", -1) { davids.delete_all }
- assert_not_predicate davids, :loaded?
- end
-
- def test_delete_all_loaded
- davids = Author.where(name: "David")
-
- # Force load
- assert_equal [authors(:david)], davids.to_a
- assert_predicate davids, :loaded?
-
- assert_difference("Author.count", -1) { davids.delete_all }
-
- assert_equal [], davids.to_a
- assert_predicate davids, :loaded?
- end
-
- def test_delete_all_with_unpermitted_relation_raises_error
- assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all }
- assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all }
- assert_raises(ActiveRecord::ActiveRecordError) { Author.having("SUM(id) < 3").delete_all }
- end
-
def test_select_with_aggregates
posts = Post.select(:title, :body)
@@ -976,18 +985,23 @@ class RelationTest < ActiveRecord::TestCase
assert_queries(1) { assert_equal 11, posts.load.size }
end
+ def test_size_with_eager_loading_and_custom_select_and_order
+ posts = Post.includes(:comments).order("comments.id").select(:type)
+ assert_queries(1) { assert_equal 11, posts.size }
+ assert_queries(1) { assert_equal 11, posts.load.size }
+ end
+
def test_size_with_eager_loading_and_custom_order_and_distinct
posts = Post.includes(:comments).order("comments.id").distinct
assert_queries(1) { assert_equal 11, posts.size }
assert_queries(1) { assert_equal 11, posts.load.size }
end
- def test_update_all_with_scope
- tag = Tag.first
- Post.tagged_with(tag.id).update_all title: "rofl"
- list = Post.tagged_with(tag.id).all.to_a
- assert_operator list.length, :>, 0
- list.each { |post| assert_equal "rofl", post.title }
+ def test_size_with_eager_loading_and_manual_distinct_select_and_custom_order
+ accounts = Account.select("DISTINCT accounts.firm_id").order("accounts.firm_id")
+
+ assert_queries(1) { assert_equal 5, accounts.size }
+ assert_queries(1) { assert_equal 5, accounts.load.size }
end
def test_count_explicit_columns
@@ -1097,7 +1111,7 @@ class RelationTest < ActiveRecord::TestCase
assert_not_predicate posts.where(id: nil), :any?
assert posts.any? { |p| p.id > 0 }
- assert ! posts.any? { |p| p.id <= 0 }
+ assert_not posts.any? { |p| p.id <= 0 }
end
assert_predicate posts, :loaded?
@@ -1109,7 +1123,7 @@ class RelationTest < ActiveRecord::TestCase
assert_queries(2) do
assert posts.many? # Uses COUNT()
assert posts.many? { |p| p.id > 0 }
- assert ! posts.many? { |p| p.id < 2 }
+ assert_not posts.many? { |p| p.id < 2 }
end
assert_predicate posts, :loaded?
@@ -1125,14 +1139,14 @@ class RelationTest < ActiveRecord::TestCase
def test_none?
posts = Post.all
assert_queries(1) do
- assert ! posts.none? # Uses COUNT()
+ assert_not posts.none? # Uses COUNT()
end
assert_not_predicate posts, :loaded?
assert_queries(1) do
assert posts.none? { |p| p.id < 0 }
- assert ! posts.none? { |p| p.id == 1 }
+ assert_not posts.none? { |p| p.id == 1 }
end
assert_predicate posts, :loaded?
@@ -1141,13 +1155,13 @@ class RelationTest < ActiveRecord::TestCase
def test_one
posts = Post.all
assert_queries(1) do
- assert ! posts.one? # Uses COUNT()
+ assert_not posts.one? # Uses COUNT()
end
assert_not_predicate posts, :loaded?
assert_queries(1) do
- assert ! posts.one? { |p| p.id < 3 }
+ assert_not posts.one? { |p| p.id < 3 }
assert posts.one? { |p| p.id == 1 }
end
@@ -1231,8 +1245,23 @@ class RelationTest < ActiveRecord::TestCase
assert_equal "green", parrot.color
end
+ def test_first_or_create_with_after_initialize
+ Bird.create!(color: "yellow", name: "canary")
+ parrot = assert_deprecated do
+ Bird.where(color: "green").first_or_create do |bird|
+ bird.name = "parrot"
+ bird.enable_count = true
+ end
+ end
+ assert_equal 0, parrot.total_count
+ end
+
def test_first_or_create_with_block
- parrot = Bird.where(color: "green").first_or_create { |bird| bird.name = "parrot" }
+ Bird.create!(color: "yellow", name: "canary")
+ parrot = Bird.where(color: "green").first_or_create do |bird|
+ bird.name = "parrot"
+ assert_deprecated { assert_equal 0, Bird.count }
+ end
assert_kind_of Bird, parrot
assert_predicate parrot, :persisted?
assert_equal "green", parrot.color
@@ -1273,8 +1302,23 @@ class RelationTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::RecordInvalid) { Bird.where(color: "green").first_or_create! }
end
+ def test_first_or_create_bang_with_after_initialize
+ Bird.create!(color: "yellow", name: "canary")
+ parrot = assert_deprecated do
+ Bird.where(color: "green").first_or_create! do |bird|
+ bird.name = "parrot"
+ bird.enable_count = true
+ end
+ end
+ assert_equal 0, parrot.total_count
+ end
+
def test_first_or_create_bang_with_valid_block
- parrot = Bird.where(color: "green").first_or_create! { |bird| bird.name = "parrot" }
+ Bird.create!(color: "yellow", name: "canary")
+ parrot = Bird.where(color: "green").first_or_create! do |bird|
+ bird.name = "parrot"
+ assert_deprecated { assert_equal 0, Bird.count }
+ end
assert_kind_of Bird, parrot
assert_predicate parrot, :persisted?
assert_equal "green", parrot.color
@@ -1323,8 +1367,23 @@ class RelationTest < ActiveRecord::TestCase
assert_equal "green", parrot.color
end
+ def test_first_or_initialize_with_after_initialize
+ Bird.create!(color: "yellow", name: "canary")
+ parrot = assert_deprecated do
+ Bird.where(color: "green").first_or_initialize do |bird|
+ bird.name = "parrot"
+ bird.enable_count = true
+ end
+ end
+ assert_equal 0, parrot.total_count
+ end
+
def test_first_or_initialize_with_block
- parrot = Bird.where(color: "green").first_or_initialize { |bird| bird.name = "parrot" }
+ Bird.create!(color: "yellow", name: "canary")
+ parrot = Bird.where(color: "green").first_or_initialize do |bird|
+ bird.name = "parrot"
+ assert_deprecated { assert_equal 0, Bird.count }
+ end
assert_kind_of Bird, parrot
assert_not_predicate parrot, :persisted?
assert_predicate parrot, :valid?
@@ -1365,6 +1424,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")
@@ -1384,6 +1450,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")
@@ -1402,15 +1500,27 @@ class RelationTest < ActiveRecord::TestCase
assert_equal "cock", hens.new.name
end
+ def test_create_with_nested_attributes
+ assert_difference("Project.count", 1) do
+ developers = Developer.where(name: "Aaron")
+ developers = developers.create_with(
+ projects_attributes: [{ name: "p1" }]
+ )
+ developers.create!
+ end
+ end
+
def test_except
relation = Post.where(author_id: 1).order("id ASC").limit(1)
assert_equal [posts(:welcome)], relation.to_a
author_posts = relation.except(:order, :limit)
- assert_equal Post.where(author_id: 1).to_a, author_posts.to_a
+ assert_equal Post.where(author_id: 1).sort_by(&:id), author_posts.sort_by(&:id)
+ assert_equal author_posts.sort_by(&:id), relation.scoping { Post.except(:order, :limit).sort_by(&:id) }
all_posts = relation.except(:where, :order, :limit)
- assert_equal Post.all, all_posts
+ assert_equal Post.all.sort_by(&:id), all_posts.sort_by(&:id)
+ assert_equal all_posts.sort_by(&:id), relation.scoping { Post.except(:where, :order, :limit).sort_by(&:id) }
end
def test_only
@@ -1418,10 +1528,12 @@ class RelationTest < ActiveRecord::TestCase
assert_equal [posts(:welcome)], relation.to_a
author_posts = relation.only(:where)
- assert_equal Post.where(author_id: 1).to_a, author_posts.to_a
+ assert_equal Post.where(author_id: 1).sort_by(&:id), author_posts.sort_by(&:id)
+ assert_equal author_posts.sort_by(&:id), relation.scoping { Post.only(:where).sort_by(&:id) }
- all_posts = relation.only(:limit)
- assert_equal Post.limit(1).to_a, all_posts.to_a
+ all_posts = relation.only(:order)
+ assert_equal Post.order("id ASC").to_a, all_posts.to_a
+ assert_equal all_posts.to_a, relation.scoping { Post.only(:order).to_a }
end
def test_anonymous_extension
@@ -1483,68 +1595,6 @@ class RelationTest < ActiveRecord::TestCase
assert_equal authors(:david), Author.order("id DESC , name DESC").last
end
- def test_update_all_with_blank_argument
- assert_raises(ArgumentError) { Comment.update_all({}) }
- end
-
- def test_update_all_with_joins
- comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id)
- count = comments.count
-
- assert_equal count, comments.update_all(post_id: posts(:thinking).id)
- assert_equal posts(:thinking), comments(:greetings).post
- end
-
- def test_update_all_with_joins_and_limit
- comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).limit(1)
- assert_equal 1, comments.update_all(post_id: posts(:thinking).id)
- end
-
- def test_update_all_with_joins_and_limit_and_order
- comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).order("comments.id").limit(1)
- assert_equal 1, comments.update_all(post_id: posts(:thinking).id)
- assert_equal posts(:thinking), comments(:greetings).post
- assert_equal posts(:welcome), comments(:more_greetings).post
- end
-
- def test_update_all_with_joins_and_offset
- all_comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id)
- count = all_comments.count
- comments = all_comments.offset(1)
-
- assert_equal count - 1, comments.update_all(post_id: posts(:thinking).id)
- end
-
- def test_update_all_with_joins_and_offset_and_order
- all_comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).order("posts.id", "comments.id")
- count = all_comments.count
- comments = all_comments.offset(1)
-
- assert_equal count - 1, comments.update_all(post_id: posts(:thinking).id)
- assert_equal posts(:thinking), comments(:more_greetings).post
- assert_equal posts(:welcome), comments(:greetings).post
- end
-
- def test_update_on_relation
- topic1 = TopicWithCallbacks.create! title: "arel", author_name: nil
- topic2 = TopicWithCallbacks.create! title: "activerecord", author_name: nil
- topics = TopicWithCallbacks.where(id: [topic1.id, topic2.id])
- topics.update(title: "adequaterecord")
-
- assert_equal "adequaterecord", topic1.reload.title
- assert_equal "adequaterecord", topic2.reload.title
- # Testing that the before_update callbacks have run
- assert_equal "David", topic1.reload.author_name
- assert_equal "David", topic2.reload.author_name
- end
-
- def test_update_on_relation_passing_active_record_object_is_not_permitted
- topic = Topic.create!(title: "Foo", author_name: nil)
- assert_raises(ArgumentError) do
- Topic.where(id: topic.id).update(topic, title: "Bar")
- end
- end
-
def test_distinct
tag1 = Tag.create(name: "Foo")
tag2 = Tag.create(name: "Foo")
@@ -1696,7 +1746,7 @@ class RelationTest < ActiveRecord::TestCase
# checking if there are topics is used before you actually display them,
# thus it shouldn't invoke an extra count query.
assert_no_queries { assert topics.present? }
- assert_no_queries { assert !topics.blank? }
+ assert_no_queries { assert_not topics.blank? }
# shows count of topics and loops after loading the query should not trigger extra queries either.
assert_no_queries { topics.size }
@@ -1709,6 +1759,24 @@ class RelationTest < ActiveRecord::TestCase
assert_predicate topics, :loaded?
end
+ def test_delete_by
+ david = authors(:david)
+
+ assert_difference("Post.count", -3) { david.posts.delete_by(body: "hello") }
+
+ deleted = Author.delete_by(id: david.id)
+ assert_equal 1, deleted
+ end
+
+ def test_destroy_by
+ david = authors(:david)
+
+ assert_difference("Post.count", -3) { david.posts.destroy_by(body: "hello") }
+
+ destroyed = Author.destroy_by(id: david.id)
+ assert_equal [david], destroyed
+ end
+
test "find_by with hash conditions returns the first matching record" do
assert_equal posts(:eager_other), Post.order(:id).find_by(author_id: 2)
end
@@ -1878,6 +1946,19 @@ class RelationTest < ActiveRecord::TestCase
assert_equal [1, 1, 1], posts.map(&:author_address_id)
end
+ test "joins with select custom attribute" do
+ contract = Company.create!(name: "test").contracts.create!
+ company = Company.joins(:contracts).select(:id, :metadata).find(contract.company_id)
+ assert_equal contract.metadata, company.metadata
+ end
+
+ test "joins with order by custom attribute" do
+ companies = Company.create!([{ name: "test1" }, { name: "test2" }])
+ companies.each { |company| company.contracts.create! }
+ assert_equal companies, Company.joins(:contracts).order(:metadata)
+ assert_equal companies.reverse, Company.joins(:contracts).order(metadata: :desc)
+ end
+
test "delegations do not leak to other classes" do
Topic.all.by_lifo
assert Topic.all.class.method_defined?(:by_lifo)
@@ -1896,6 +1977,30 @@ class RelationTest < ActiveRecord::TestCase
assert_equal p2.first.comments, comments
end
+ def test_unscope_with_merge
+ p0 = Post.where(author_id: 0)
+ p1 = Post.where(author_id: 1, comments_count: 1)
+
+ assert_equal [posts(:authorless)], p0
+ assert_equal [posts(:thinking)], p1
+
+ comments = Comment.merge(p0).unscope(where: :author_id).where(post: p1)
+
+ assert_not_equal p0.first.comments, comments
+ assert_equal p1.first.comments, comments
+ end
+
+ def test_unscope_with_unknown_column
+ comment = comments(:greetings)
+ comment.update!(comments: 1)
+
+ comments = Comment.where(comments: 1).unscope(where: :unknown_column)
+ assert_equal [comment], comments
+
+ comments = Comment.where(comments: 1).unscope(where: { comments: :unknown_column })
+ assert_equal [comment], comments
+ end
+
def test_unscope_specific_where_value
posts = Post.where(title: "Welcome to the weblog", body: "Such a lovely day")
@@ -1914,6 +2019,16 @@ class RelationTest < ActiveRecord::TestCase
assert_equal "Thank you for the welcome,Thank you again for the welcome", Post.first.comments.join(",")
end
+ def test_relation_with_private_kernel_method
+ accounts = Account.all
+ assert_equal [accounts(:signals37)], accounts.open
+ assert_equal [accounts(:signals37)], accounts.available
+
+ sub_accounts = SubAccount.all
+ assert_equal [accounts(:signals37)], sub_accounts.open
+ assert_equal [accounts(:signals37)], sub_accounts.available
+ end
+
test "#skip_query_cache!" do
Post.cache do
assert_queries(1) do
diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb
index db52c108ac..825aee2423 100644
--- a/activerecord/test/cases/result_test.rb
+++ b/activerecord/test/cases/result_test.rb
@@ -12,16 +12,31 @@ module ActiveRecord
])
end
+ test "includes_column?" do
+ assert result.includes_column?("col_1")
+ assert_not result.includes_column?("foo")
+ end
+
test "length" do
assert_equal 3, result.length
end
- test "to_hash returns row_hashes" do
+ test "to_a returns row_hashes" do
assert_equal [
{ "col_1" => "row 1 col 1", "col_2" => "row 1 col 2" },
{ "col_1" => "row 2 col 1", "col_2" => "row 2 col 2" },
{ "col_1" => "row 3 col 1", "col_2" => "row 3 col 2" },
- ], result.to_hash
+ ], result.to_a
+ end
+
+ test "to_hash (deprecated) returns row_hashes" do
+ assert_deprecated do
+ assert_equal [
+ { "col_1" => "row 1 col 1", "col_2" => "row 1 col 2" },
+ { "col_1" => "row 2 col 1", "col_2" => "row 2 col 2" },
+ { "col_1" => "row 3 col 1", "col_2" => "row 3 col 2" },
+ ], result.to_hash
+ end
end
test "first returns first row as a hash" do
diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb
index 778cf86ac3..6c884b4f45 100644
--- a/activerecord/test/cases/sanitize_test.rb
+++ b/activerecord/test/cases/sanitize_test.rb
@@ -148,6 +148,19 @@ class SanitizeTest < ActiveRecord::TestCase
assert_equal "foo in (#{quoted_nil})", bind("foo in (?)", [])
end
+ def test_bind_range
+ quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')})
+ assert_equal "0", bind("?", 0..0)
+ assert_equal "1,2,3", bind("?", 1..3)
+ assert_equal quoted_abc, bind("?", "a"..."d")
+ end
+
+ def test_bind_empty_range
+ quoted_nil = ActiveRecord::Base.connection.quote(nil)
+ assert_equal quoted_nil, bind("?", 0...0)
+ assert_equal quoted_nil, bind("?", "a"..."a")
+ end
+
def test_bind_empty_string
quoted_empty = ActiveRecord::Base.connection.quote("")
assert_equal quoted_empty, bind("?", "")
@@ -168,12 +181,6 @@ class SanitizeTest < ActiveRecord::TestCase
assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call
end
- def test_deprecated_expand_hash_conditions_for_aggregates
- assert_deprecated do
- assert_equal({ "balance" => 50 }, Customer.send(:expand_hash_conditions_for_aggregates, balance: Money.new(50)))
- end
- end
-
private
def bind(statement, *vars)
if vars.first.is_a?(Hash)
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index 31bdf3f357..49e9be9565 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -37,24 +37,6 @@ class SchemaDumperTest < ActiveRecord::TestCase
ActiveRecord::SchemaMigration.delete_all
end
- if current_adapter?(:SQLite3Adapter)
- %w{3.7.8 3.7.11 3.7.12}.each do |version_string|
- test "dumps schema version for sqlite version #{version_string}" do
- version = ActiveRecord::ConnectionAdapters::SQLite3Adapter::Version.new(version_string)
- ActiveRecord::Base.connection.stubs(:sqlite_version).returns(version)
-
- versions = %w{ 20100101010101 20100201010101 20100301010101 }
- versions.reverse_each do |v|
- ActiveRecord::SchemaMigration.create!(version: v)
- end
-
- schema_info = ActiveRecord::Base.connection.dump_schema_information
- assert_match(/20100201010101.*20100301010101/m, schema_info)
- ActiveRecord::SchemaMigration.delete_all
- end
- end
- end
-
def test_schema_dump
output = standard_dump
assert_match %r{create_table "accounts"}, output
@@ -192,7 +174,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dumps_partial_indices
index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_partial_index/).first.strip
- if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index?
+ if ActiveRecord::Base.connection.supports_partial_index?
assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)"', index_definition
else
assert_equal 't.index ["firm_id", "type"], name: "company_partial_index"', index_definition
@@ -244,27 +226,50 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{t\.float\s+"temperature"$}, output
end
+ 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\).+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\).+END\) DESC"\z}i, index_definition
+ else
+ assert false
+ end
+ end
+ end
+
if current_adapter?(:Mysql2Adapter)
def test_schema_dump_includes_length_for_mysql_binary_fields
- output = standard_dump
+ output = dump_table_schema "binary_fields"
assert_match %r{t\.binary\s+"var_binary",\s+limit: 255$}, output
assert_match %r{t\.binary\s+"var_binary_large",\s+limit: 4095$}, output
end
def test_schema_dump_includes_length_for_mysql_blob_and_text_fields
- output = standard_dump
- assert_match %r{t\.blob\s+"tiny_blob",\s+limit: 255$}, output
+ output = dump_table_schema "binary_fields"
+ assert_match %r{t\.binary\s+"tiny_blob",\s+size: :tiny$}, output
assert_match %r{t\.binary\s+"normal_blob"$}, output
- assert_match %r{t\.binary\s+"medium_blob",\s+limit: 16777215$}, output
- assert_match %r{t\.binary\s+"long_blob",\s+limit: 4294967295$}, output
- assert_match %r{t\.text\s+"tiny_text",\s+limit: 255$}, output
+ assert_match %r{t\.binary\s+"medium_blob",\s+size: :medium$}, output
+ assert_match %r{t\.binary\s+"long_blob",\s+size: :long$}, output
+ assert_match %r{t\.text\s+"tiny_text",\s+size: :tiny$}, output
assert_match %r{t\.text\s+"normal_text"$}, output
- assert_match %r{t\.text\s+"medium_text",\s+limit: 16777215$}, output
- assert_match %r{t\.text\s+"long_text",\s+limit: 4294967295$}, output
+ assert_match %r{t\.text\s+"medium_text",\s+size: :medium$}, output
+ assert_match %r{t\.text\s+"long_text",\s+size: :long$}, output
+ assert_match %r{t\.binary\s+"tiny_blob_2",\s+size: :tiny$}, output
+ assert_match %r{t\.binary\s+"medium_blob_2",\s+size: :medium$}, output
+ assert_match %r{t\.binary\s+"long_blob_2",\s+size: :long$}, output
+ assert_match %r{t\.text\s+"tiny_text_2",\s+size: :tiny$}, output
+ assert_match %r{t\.text\s+"medium_text_2",\s+size: :medium$}, output
+ assert_match %r{t\.text\s+"long_text_2",\s+size: :long$}, output
end
def test_schema_does_not_include_limit_for_emulated_mysql_boolean_fields
- output = standard_dump
+ output = dump_table_schema "booleans"
assert_no_match %r{t\.boolean\s+"has_fun",.+limit: 1}, output
end
@@ -296,11 +301,6 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{t\.decimal\s+"decimal_array_default",\s+default: \["1.23", "3.45"\],\s+array: true}, output
end
- def test_schema_dump_expression_indices
- index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_expression_index/).first.strip
- assert_match %r{CASE.+lower\(\(name\)::text\)}i, index_definition
- end
-
def test_schema_dump_interval_type
output = dump_table_schema "postgresql_times"
assert_match %r{t\.interval\s+"time_interval"$}, output
@@ -315,29 +315,33 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dump_includes_extensions
connection = ActiveRecord::Base.connection
- connection.stubs(:extensions).returns(["hstore"])
- output = perform_schema_dump
- assert_match "# These are extensions that must be enabled", output
- assert_match %r{enable_extension "hstore"}, output
+ connection.stub(:extensions, ["hstore"]) do
+ output = perform_schema_dump
+ assert_match "# These are extensions that must be enabled", output
+ assert_match %r{enable_extension "hstore"}, output
+ end
- connection.stubs(:extensions).returns([])
- output = perform_schema_dump
- assert_no_match "# These are extensions that must be enabled", output
- assert_no_match %r{enable_extension}, output
+ connection.stub(:extensions, []) do
+ output = perform_schema_dump
+ assert_no_match "# These are extensions that must be enabled", output
+ assert_no_match %r{enable_extension}, output
+ end
end
def test_schema_dump_includes_extensions_in_alphabetic_order
connection = ActiveRecord::Base.connection
- connection.stubs(:extensions).returns(["hstore", "uuid-ossp", "xml2"])
- output = perform_schema_dump
- enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten
- assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions
+ connection.stub(:extensions, ["hstore", "uuid-ossp", "xml2"]) do
+ output = perform_schema_dump
+ enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten
+ assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions
+ end
- connection.stubs(:extensions).returns(["uuid-ossp", "xml2", "hstore"])
- output = perform_schema_dump
- enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten
- assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions
+ connection.stub(:extensions, ["uuid-ossp", "xml2", "hstore"]) do
+ output = perform_schema_dump
+ enabled_extensions = output.scan(%r{enable_extension "(.+)"}).flatten
+ assert_equal ["hstore", "uuid-ossp", "xml2"], enabled_extensions
+ end
end
end
diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb
index 0804de1fb3..e7bdab58c6 100644
--- a/activerecord/test/cases/scoping/default_scoping_test.rb
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -4,6 +4,7 @@ require "cases/helper"
require "models/post"
require "models/comment"
require "models/developer"
+require "models/project"
require "models/computer"
require "models/vehicle"
require "models/cat"
@@ -193,7 +194,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
def test_order_to_unscope_reordering
scope = DeveloperOrderedBySalary.order("salary DESC, name ASC").reverse_order.unscope(:order)
- assert !/order/i.match?(scope.to_sql)
+ assert_no_match(/order/i, scope.to_sql)
end
def test_unscope_reverse_order
@@ -366,6 +367,21 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal "Jamis", jamis.name
end
+ def test_create_with_takes_precedence_over_where
+ developer = Developer.where(name: nil).create_with(name: "Aaron").new
+ assert_equal "Aaron", developer.name
+ end
+
+ def test_create_with_nested_attributes
+ assert_difference("Project.count", 1) do
+ Developer.create_with(
+ projects_attributes: [{ name: "p1" }]
+ ).scoping do
+ Developer.create!(name: "Aaron")
+ end
+ end
+ end
+
# FIXME: I don't know if this is *desired* behavior, but it is *today's*
# behavior.
def test_create_with_empty_hash_will_not_reset
@@ -392,18 +408,18 @@ class DefaultScopingTest < ActiveRecord::TestCase
end
def test_joins_not_affected_by_scope_other_than_default_or_unscoped
- without_scope_on_post = Comment.joins(:post).to_a
+ without_scope_on_post = Comment.joins(:post).sort_by(&:id)
with_scope_on_post = nil
Post.where(id: [1, 5, 6]).scoping do
- with_scope_on_post = Comment.joins(:post).to_a
+ with_scope_on_post = Comment.joins(:post).sort_by(&:id)
end
- assert_equal with_scope_on_post, without_scope_on_post
+ assert_equal without_scope_on_post, with_scope_on_post
end
def test_unscoped_with_joins_should_not_have_default_scope
- assert_equal SpecialPostWithDefaultScope.unscoped { Comment.joins(:special_post_with_default_scope).to_a },
- Comment.joins(:post).to_a
+ assert_equal Comment.joins(:post).sort_by(&:id),
+ SpecialPostWithDefaultScope.unscoped { Comment.joins(:special_post_with_default_scope).sort_by(&:id) }
end
def test_sti_association_with_unscoped_not_affected_by_default_scope
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index 03f8a4f7c9..3488442cab 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -50,7 +50,7 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_calling_merge_at_first_in_scope
Topic.class_eval do
- scope :calling_merge_at_first_in_scope, Proc.new { merge(Topic.replied) }
+ scope :calling_merge_at_first_in_scope, Proc.new { merge(Topic.unscoped.replied) }
end
assert_equal Topic.calling_merge_at_first_in_scope.to_a, Topic.replied.to_a
end
@@ -86,7 +86,7 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_scopes_are_composable
assert_equal((approved = Topic.all.merge!(where: { approved: true }).to_a), Topic.approved)
assert_equal((replied = Topic.all.merge!(where: "replies_count > 0").to_a), Topic.replied)
- assert !(approved == replied)
+ assert_not (approved == replied)
assert_not_empty (approved & replied)
assert_equal approved & replied, Topic.approved.replied
@@ -303,13 +303,6 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal "lifo", topic.author_name
end
- def test_deprecated_delegating_private_method
- assert_deprecated do
- scope = Topic.all.by_private_lifo
- assert_not scope.instance_variable_get(:@delegate_to_klass)
- end
- end
-
def test_reserved_scope_names
klass = Class.new(ActiveRecord::Base) do
self.table_name = "topics"
@@ -454,6 +447,17 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).to_a.uniq
end
+ def test_class_method_in_scope
+ assert_deprecated do
+ assert_equal [topics(:second)], topics(:first).approved_replies.ordered
+ end
+ end
+
+ def test_nested_scoping
+ expected = Reply.approved
+ assert_equal expected.to_a, Topic.rejected.nested_scoping(expected)
+ end
+
def test_scopes_batch_finders
assert_equal 4, Topic.approved.count
@@ -488,8 +492,9 @@ class NamedScopingTest < ActiveRecord::TestCase
[:public_method, :protected_method, :private_method].each do |reserved_method|
assert Topic.respond_to?(reserved_method, true)
- ActiveRecord::Base.logger.expects(:warn)
- silence_warnings { Topic.scope(reserved_method, -> {}) }
+ assert_called(ActiveRecord::Base.logger, :warn) do
+ silence_warnings { Topic.scope(reserved_method, -> { }) }
+ end
end
end
@@ -597,4 +602,14 @@ class NamedScopingTest < ActiveRecord::TestCase
Topic.create!
assert_predicate Topic, :one?
end
+
+ def test_scope_with_annotation
+ Topic.class_eval do
+ scope :including_annotate_in_scope, Proc.new { annotate("from-scope") }
+ end
+
+ assert_sql(%r{/\* from-scope \*/}) do
+ assert Topic.including_annotate_in_scope.to_a, Topic.all.to_a
+ end
+ end
end
diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb
index 5c86bc892d..50b514d464 100644
--- a/activerecord/test/cases/scoping/relation_scoping_test.rb
+++ b/activerecord/test/cases/scoping/relation_scoping_test.rb
@@ -105,7 +105,7 @@ class RelationScopingTest < ActiveRecord::TestCase
Developer.select("id, name").scoping do
developer = Developer.where("name = 'David'").first
assert_equal "David", developer.name
- assert !developer.has_attribute?(:salary)
+ assert_not developer.has_attribute?(:salary)
end
end
@@ -130,6 +130,44 @@ class RelationScopingTest < ActiveRecord::TestCase
end
end
+ def test_scoped_find_with_annotation
+ Developer.annotate("scoped").scoping do
+ developer = nil
+ assert_sql(%r{/\* scoped \*/}) do
+ developer = Developer.where("name = 'David'").first
+ end
+ assert_equal "David", developer.name
+ end
+ end
+
+ def test_find_with_annotation_unscoped
+ Developer.annotate("scoped").unscoped do
+ developer = nil
+ log = capture_sql do
+ developer = Developer.where("name = 'David'").first
+ end
+
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\* scoped \*/}) }, :empty?
+
+ assert_equal "David", developer.name
+ end
+ end
+
+ def test_find_with_annotation_unscope
+ developer = nil
+ log = capture_sql do
+ developer = Developer.annotate("unscope").
+ where("name = 'David'").
+ unscope(:annotate).first
+ end
+
+ assert_not_predicate log, :empty?
+ assert_predicate log.select { |query| query.match?(%r{/\* unscope \*/}) }, :empty?
+
+ assert_equal "David", developer.name
+ end
+
def test_scoped_find_include
# with the include, will retrieve only developers for the given project
scoped_developers = Developer.includes(:projects).scoping do
@@ -236,8 +274,8 @@ class RelationScopingTest < ActiveRecord::TestCase
SpecialComment.unscoped.created
end
- assert_nil Comment.current_scope
- assert_nil SpecialComment.current_scope
+ assert_nil Comment.send(:current_scope)
+ assert_nil SpecialComment.send(:current_scope)
end
def test_scoping_respects_current_class
@@ -254,6 +292,16 @@ class RelationScopingTest < ActiveRecord::TestCase
end
end
+ 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)
@@ -363,7 +411,19 @@ class HasManyScopingTest < ActiveRecord::TestCase
def test_nested_scope_finder
Comment.where("1=0").scoping do
- assert_equal 0, @welcome.comments.count
+ assert_equal 2, @welcome.comments.count
+ assert_equal "a comment...", @welcome.comments.what_are_you
+ end
+
+ Comment.where("1=1").scoping do
+ assert_equal 2, @welcome.comments.count
+ assert_equal "a comment...", @welcome.comments.what_are_you
+ end
+ end
+
+ def test_none_scoping
+ Comment.none.scoping do
+ assert_equal 2, @welcome.comments.count
assert_equal "a comment...", @welcome.comments.what_are_you
end
@@ -404,7 +464,19 @@ class HasAndBelongsToManyScopingTest < ActiveRecord::TestCase
def test_nested_scope_finder
Category.where("1=0").scoping do
- assert_equal 0, @welcome.categories.count
+ assert_equal 2, @welcome.categories.count
+ assert_equal "a category...", @welcome.categories.what_are_you
+ end
+
+ Category.where("1=1").scoping do
+ assert_equal 2, @welcome.categories.count
+ assert_equal "a category...", @welcome.categories.what_are_you
+ end
+ end
+
+ def test_none_scoping
+ Category.none.scoping do
+ assert_equal 2, @welcome.categories.count
assert_equal "a category...", @welcome.categories.what_are_you
end
diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb
index 2d829ad4ba..932780bfef 100644
--- a/activerecord/test/cases/serialization_test.rb
+++ b/activerecord/test/cases/serialization_test.rb
@@ -67,8 +67,8 @@ class SerializationTest < ActiveRecord::TestCase
klazz.include_root_in_json = false
assert ActiveRecord::Base.include_root_in_json
- assert !klazz.include_root_in_json
- assert !klazz.new.include_root_in_json
+ assert_not klazz.include_root_in_json
+ assert_not klazz.new.include_root_in_json
ensure
ActiveRecord::Base.include_root_in_json = original_root_in_json
end
diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb
index 7de5429cbb..ecf81b2042 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -1,18 +1,23 @@
# frozen_string_literal: true
require "cases/helper"
-require "models/topic"
-require "models/reply"
require "models/person"
require "models/traffic_light"
require "models/post"
-require "bcrypt"
class SerializedAttributeTest < ActiveRecord::TestCase
fixtures :topics, :posts
MyObject = Struct.new :attribute1, :attribute2
+ class Topic < ActiveRecord::Base
+ serialize :content
+ end
+
+ class ImportantTopic < Topic
+ serialize :important, Hash
+ end
+
teardown do
Topic.serialize("content")
end
@@ -49,10 +54,10 @@ class SerializedAttributeTest < ActiveRecord::TestCase
def test_serialized_attributes_from_database_on_subclass
Topic.serialize :content, Hash
- t = Reply.new(content: { foo: :bar })
+ t = ImportantTopic.new(content: { foo: :bar })
assert_equal({ foo: :bar }, t.content)
t.save!
- t = Reply.last
+ t = ImportantTopic.last
assert_equal({ foo: :bar }, t.content)
end
@@ -159,6 +164,13 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_equal(settings, Topic.find(topic.id).content)
end
+ def test_where_by_serialized_attribute_with_array
+ settings = [ "color" => "green" ]
+ Topic.serialize(:content, Array)
+ topic = Topic.create!(content: settings)
+ assert_equal topic, Topic.where(content: settings).take
+ end
+
def test_where_by_serialized_attribute_with_hash
settings = { "color" => "green" }
Topic.serialize(:content, Hash)
@@ -166,6 +178,13 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_equal topic, Topic.where(content: settings).take
end
+ def test_where_by_serialized_attribute_with_hash_in_array
+ settings = { "color" => "green" }
+ Topic.serialize(:content, Hash)
+ topic = Topic.create!(content: settings)
+ assert_equal topic, Topic.where(content: [settings]).take
+ end
+
def test_serialized_default_class
Topic.serialize(:content, Hash)
topic = Topic.new
@@ -308,7 +327,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
topic = Topic.create!(content: {})
topic2 = Topic.create!(content: nil)
- assert_equal [topic, topic2], Topic.where(content: nil)
+ assert_equal [topic, topic2], Topic.where(content: nil).sort_by(&:id)
end
def test_nil_is_always_persisted_as_null
@@ -353,9 +372,9 @@ class SerializedAttributeTest < ActiveRecord::TestCase
end
def test_serialized_attribute_works_under_concurrent_initial_access
- model = Topic.dup
+ model = Class.new(Topic)
- topic = model.last
+ topic = model.create!
topic.update group: "1"
model.serialize :group, JSON
diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb
index e3c12f68fd..6a6d73dc38 100644
--- a/activerecord/test/cases/statement_cache_test.rb
+++ b/activerecord/test/cases/statement_cache_test.rb
@@ -4,6 +4,7 @@ require "cases/helper"
require "models/book"
require "models/liquid"
require "models/molecule"
+require "models/numeric_data"
require "models/electron"
module ActiveRecord
@@ -74,6 +75,11 @@ module ActiveRecord
assert_equal "salty", liquids[0].name
end
+ def test_statement_cache_with_strictly_cast_attribute
+ row = NumericData.create(temperature: 1.5)
+ assert_equal row, NumericData.find_by(temperature: 1.5)
+ end
+
def test_statement_cache_values_differ
cache = ActiveRecord::StatementCache.create(Book.connection) do |params|
Book.where(name: "my book")
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/store_test.rb b/activerecord/test/cases/store_test.rb
index 3bd480cfbd..91c0e959f4 100644
--- a/activerecord/test/cases/store_test.rb
+++ b/activerecord/test/cases/store_test.rb
@@ -79,6 +79,74 @@ class StoreTest < ActiveRecord::TestCase
assert_not_predicate @john, :settings_changed?
end
+ test "updating the store will mark accessor as changed" do
+ @john.color = "red"
+ assert @john.color_changed?
+ end
+
+ test "new record and no accessors changes" do
+ user = Admin::User.new
+ assert_not user.color_changed?
+ assert_nil user.color_was
+ assert_nil user.color_change
+
+ user.color = "red"
+ assert user.color_changed?
+ assert_nil user.color_was
+ assert_equal "red", user.color_change[1]
+ end
+
+ test "updating the store won't mark accessor as changed if the whole store was updated" do
+ @john.settings = { color: @john.color, some: "thing" }
+ assert @john.settings_changed?
+ assert_not @john.color_changed?
+ end
+
+ test "updating the store populates the accessor changed array correctly" do
+ @john.color = "red"
+ assert_equal "black", @john.color_was
+ assert_equal "black", @john.color_change[0]
+ assert_equal "red", @john.color_change[1]
+ end
+
+ test "updating the store won't mark accessor as changed if the value isn't changed" do
+ @john.color = @john.color
+ assert_not @john.color_changed?
+ end
+
+ test "nullifying the store mark accessor as changed" do
+ color = @john.color
+ @john.settings = nil
+ assert @john.color_changed?
+ assert_equal color, @john.color_was
+ assert_equal [color, nil], @john.color_change
+ end
+
+ test "dirty methods for suffixed accessors" do
+ @john.configs[:two_factor_auth] = true
+ assert @john.two_factor_auth_configs_changed?
+ assert_nil @john.two_factor_auth_configs_was
+ assert_equal [nil, true], @john.two_factor_auth_configs_change
+ end
+
+ test "dirty methods for prefixed accessors" do
+ @john.spouse[:name] = "Lena"
+ assert @john.partner_name_changed?
+ assert_equal "Dallas", @john.partner_name_was
+ assert_equal ["Dallas", "Lena"], @john.partner_name_change
+ end
+
+ test "saved changes tracking for accessors" do
+ @john.spouse[:name] = "Lena"
+ assert @john.partner_name_changed?
+
+ @john.save!
+ assert_not @john.partner_name_change
+ assert @john.saved_change_to_partner_name?
+ assert_equal ["Dallas", "Lena"], @john.saved_change_to_partner_name
+ assert_equal "Dallas", @john.partner_name_before_last_save
+ end
+
test "object initialization with not nullable column" do
assert_equal true, @john.remember_login
end
@@ -214,4 +282,38 @@ class StoreTest < ActiveRecord::TestCase
second_dump = YAML.dump(loaded)
assert_equal @john, YAML.load(second_dump)
end
+
+ test "read store attributes through accessors with default suffix" do
+ @john.configs[:two_factor_auth] = true
+ assert_equal true, @john.two_factor_auth_configs
+ end
+
+ test "write store attributes through accessors with default suffix" do
+ @john.two_factor_auth_configs = false
+ assert_equal false, @john.configs[:two_factor_auth]
+ end
+
+ test "read store attributes through accessors with custom suffix" do
+ @john.configs[:login_retry] = 3
+ assert_equal 3, @john.login_retry_config
+ end
+
+ test "write store attributes through accessors with custom suffix" do
+ @john.login_retry_config = 5
+ assert_equal 5, @john.configs[:login_retry]
+ end
+
+ test "read accessor without pre/suffix in the same store as other pre/suffixed accessors still works" do
+ @john.configs[:secret_question] = "What is your high school?"
+ assert_equal "What is your high school?", @john.secret_question
+ end
+
+ test "write accessor without pre/suffix in the same store as other pre/suffixed accessors still works" do
+ @john.secret_question = "What was the Rails version when you first worked on it?"
+ assert_equal "What was the Rails version when you first worked on it?", @john.configs[:secret_question]
+ end
+
+ test "prefix/suffix do not affect stored attributes" do
+ assert_equal [:secret_question, :two_factor_auth, :login_retry], Admin::User.stored_attributes[:configs]
+ end
end
diff --git a/activerecord/test/cases/suppressor_test.rb b/activerecord/test/cases/suppressor_test.rb
index b68f0033d9..9be5356901 100644
--- a/activerecord/test/cases/suppressor_test.rb
+++ b/activerecord/test/cases/suppressor_test.rb
@@ -66,7 +66,7 @@ class SuppressorTest < ActiveRecord::TestCase
def test_suppresses_when_nested_multiple_times
assert_no_difference -> { Notification.count } do
Notification.suppress do
- Notification.suppress {}
+ Notification.suppress { }
Notification.create
Notification.create!
Notification.new.save
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index 48d1fc7eb0..dd4a0b0455 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -2,14 +2,23 @@
require "cases/helper"
require "active_record/tasks/database_tasks"
+require "models/author"
module ActiveRecord
module DatabaseTasksSetupper
def setup
- @mysql_tasks, @postgresql_tasks, @sqlite_tasks = stub, stub, stub
- ActiveRecord::Tasks::MySQLDatabaseTasks.stubs(:new).returns @mysql_tasks
- ActiveRecord::Tasks::PostgreSQLDatabaseTasks.stubs(:new).returns @postgresql_tasks
- ActiveRecord::Tasks::SQLiteDatabaseTasks.stubs(:new).returns @sqlite_tasks
+ @mysql_tasks, @postgresql_tasks, @sqlite_tasks = Array.new(
+ 3,
+ Class.new do
+ def create; end
+ def drop; end
+ def purge; end
+ def charset; end
+ def collation; end
+ def structure_dump(*); end
+ def structure_load(*); end
+ end.new
+ )
$stdout, @original_stdout = StringIO.new, $stdout
$stderr, @original_stderr = StringIO.new, $stderr
@@ -18,6 +27,16 @@ module ActiveRecord
def teardown
$stdout, $stderr = @original_stdout, @original_stderr
end
+
+ def with_stubbed_new
+ ActiveRecord::Tasks::MySQLDatabaseTasks.stub(:new, @mysql_tasks) do
+ ActiveRecord::Tasks::PostgreSQLDatabaseTasks.stub(:new, @postgresql_tasks) do
+ ActiveRecord::Tasks::SQLiteDatabaseTasks.stub(:new, @sqlite_tasks) do
+ yield
+ end
+ end
+ end
+ end
end
ADAPTERS_TASKS = {
@@ -28,45 +47,67 @@ module ActiveRecord
class DatabaseTasksUtilsTask < ActiveRecord::TestCase
def test_raises_an_error_when_called_with_protected_environment
- ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1)
-
protected_environments = ActiveRecord::Base.protected_environments
current_env = ActiveRecord::Base.connection.migration_context.current_environment
- assert_not_includes protected_environments, current_env
- # Assert no error
- ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
- ActiveRecord::Base.protected_environments = [current_env]
- assert_raise(ActiveRecord::ProtectedEnvironmentError) do
+ InternalMetadata[:environment] = current_env
+
+ assert_called_on_instance_of(
+ ActiveRecord::MigrationContext,
+ :current_version,
+ times: 6,
+ returns: 1
+ ) do
+ assert_not_includes protected_environments, current_env
+ # Assert no error
ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+
+ ActiveRecord::Base.protected_environments = [current_env]
+
+ assert_raise(ActiveRecord::ProtectedEnvironmentError) do
+ ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+ end
end
ensure
ActiveRecord::Base.protected_environments = protected_environments
end
def test_raises_an_error_when_called_with_protected_environment_which_name_is_a_symbol
- ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1)
-
protected_environments = ActiveRecord::Base.protected_environments
current_env = ActiveRecord::Base.connection.migration_context.current_environment
- assert_not_includes protected_environments, current_env
- # Assert no error
- ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
- ActiveRecord::Base.protected_environments = [current_env.to_sym]
- assert_raise(ActiveRecord::ProtectedEnvironmentError) do
+ InternalMetadata[:environment] = current_env
+
+ assert_called_on_instance_of(
+ ActiveRecord::MigrationContext,
+ :current_version,
+ times: 6,
+ returns: 1
+ ) do
+ assert_not_includes protected_environments, current_env
+ # Assert no error
ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+
+ ActiveRecord::Base.protected_environments = [current_env.to_sym]
+ assert_raise(ActiveRecord::ProtectedEnvironmentError) do
+ ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+ end
end
ensure
ActiveRecord::Base.protected_environments = protected_environments
end
def test_raises_an_error_if_no_migrations_have_been_made
- ActiveRecord::InternalMetadata.stubs(:table_exists?).returns(false)
- ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1)
-
- assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do
- ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+ ActiveRecord::InternalMetadata.stub(:table_exists?, false) do
+ assert_called_on_instance_of(
+ ActiveRecord::MigrationContext,
+ :current_version,
+ returns: 1
+ ) do
+ assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do
+ ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+ end
+ end
end
end
end
@@ -79,11 +120,12 @@ module ActiveRecord
end
instance = klazz.new
- klazz.stubs(:new).returns instance
- instance.expects(:structure_dump).with("awesome-file.sql", nil)
-
- ActiveRecord::Tasks::DatabaseTasks.register_task(/foo/, klazz)
- ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => :foo }, "awesome-file.sql")
+ klazz.stub(:new, instance) do
+ assert_called_with(instance, :structure_dump, ["awesome-file.sql", nil]) do
+ ActiveRecord::Tasks::DatabaseTasks.register_task(/foo/, klazz)
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => :foo }, "awesome-file.sql")
+ end
+ end
end
def test_unregistered_task
@@ -98,8 +140,11 @@ module ActiveRecord
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_create") do
- eval("@#{v}").expects(:create)
- ActiveRecord::Tasks::DatabaseTasks.create "adapter" => k
+ with_stubbed_new do
+ assert_called(eval("@#{v}"), :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create "adapter" => k
+ end
+ end
end
end
end
@@ -119,59 +164,88 @@ module ActiveRecord
def setup
@configurations = { "development" => { "database" => "my-db" } }
- ActiveRecord::Base.stubs(:configurations).returns(@configurations)
- # To refrain from connecting to a newly created empty DB in sqlite3_mem tests
- ActiveRecord::Base.connection_handler.stubs(:establish_connection)
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
end
- def test_ignores_configurations_without_databases
- @configurations["development"].merge!("database" => nil)
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
+ end
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).never
+ def test_ignores_configurations_without_databases
+ @configurations["development"]["database"] = nil
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ with_stubbed_configurations_establish_connection do
+ assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
+ end
end
def test_ignores_remote_databases
- @configurations["development"].merge!("host" => "my.server.tld")
- $stderr.stubs(:puts).returns(nil)
-
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).never
+ @configurations["development"]["host"] = "my.server.tld"
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ with_stubbed_configurations_establish_connection do
+ assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
+ end
end
def test_warning_for_remote_databases
- @configurations["development"].merge!("host" => "my.server.tld")
+ @configurations["development"]["host"] = "my.server.tld"
- $stderr.expects(:puts).with("This task only modifies local databases. my-db is on a remote host.")
+ with_stubbed_configurations_establish_connection do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ assert_match "This task only modifies local databases. my-db is on a remote host.",
+ $stderr.string
+ end
end
def test_creates_configurations_with_local_ip
- @configurations["development"].merge!("host" => "127.0.0.1")
+ @configurations["development"]["host"] = "127.0.0.1"
- ActiveRecord::Tasks::DatabaseTasks.expects(:create)
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ with_stubbed_configurations_establish_connection do
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
+ end
end
def test_creates_configurations_with_local_host
- @configurations["development"].merge!("host" => "localhost")
-
- ActiveRecord::Tasks::DatabaseTasks.expects(:create)
+ @configurations["development"]["host"] = "localhost"
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ with_stubbed_configurations_establish_connection do
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
+ end
end
def test_creates_configurations_with_blank_hosts
- @configurations["development"].merge!("host" => nil)
+ @configurations["development"]["host"] = nil
- ActiveRecord::Tasks::DatabaseTasks.expects(:create)
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ with_stubbed_configurations_establish_connection do
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
+ end
end
+
+ private
+ def with_stubbed_configurations_establish_connection
+ old_configurations = ActiveRecord::Base.configurations
+ ActiveRecord::Base.configurations = @configurations
+
+ # To refrain from connecting to a newly created empty DB in
+ # sqlite3_mem tests
+ ActiveRecord::Base.connection_handler.stub(:establish_connection, nil) do
+ yield
+ end
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
end
class DatabaseTasksCreateCurrentTest < ActiveRecord::TestCase
@@ -179,66 +253,98 @@ module ActiveRecord
@configurations = {
"development" => { "database" => "dev-db" },
"test" => { "database" => "test-db" },
- "production" => { "url" => "prod-db-url" }
+ "production" => { "url" => "abstract://prod-db-host/prod-db" }
}
-
- ActiveRecord::Base.stubs(:configurations).returns(@configurations)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
end
def test_creates_current_environment_database
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "test-db")
-
- ActiveRecord::Tasks::DatabaseTasks.create_current(
- ActiveSupport::StringInquirer.new("test")
- )
+ with_stubbed_configurations_establish_connection do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ ["database" => "test-db"],
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("test")
+ )
+ end
+ end
end
def test_creates_current_environment_database_with_url
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("url" => "prod-db-url")
-
- ActiveRecord::Tasks::DatabaseTasks.create_current(
- ActiveSupport::StringInquirer.new("production")
- )
+ with_stubbed_configurations_establish_connection do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"],
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("production")
+ )
+ end
+ end
end
def test_creates_test_and_development_databases_when_env_was_not_specified
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "dev-db")
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "test-db")
-
- ActiveRecord::Tasks::DatabaseTasks.create_current(
- ActiveSupport::StringInquirer.new("development")
- )
+ with_stubbed_configurations_establish_connection do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ [
+ ["database" => "dev-db"],
+ ["database" => "test-db"]
+ ],
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
end
def test_creates_test_and_development_databases_when_rails_env_is_development
old_env = ENV["RAILS_ENV"]
ENV["RAILS_ENV"] = "development"
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "dev-db")
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "test-db")
- ActiveRecord::Tasks::DatabaseTasks.create_current(
- ActiveSupport::StringInquirer.new("development")
- )
+ with_stubbed_configurations_establish_connection do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ [
+ ["database" => "dev-db"],
+ ["database" => "test-db"]
+ ],
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
ensure
ENV["RAILS_ENV"] = old_env
end
def test_establishes_connection_for_the_given_environments
- ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true
+ ActiveRecord::Tasks::DatabaseTasks.stub(:create, nil) do
+ assert_called_with(ActiveRecord::Base, :establish_connection, [:development]) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
+ end
- ActiveRecord::Base.expects(:establish_connection).with(:development)
+ private
+ def with_stubbed_configurations_establish_connection
+ old_configurations = ActiveRecord::Base.configurations
+ ActiveRecord::Base.configurations = @configurations
- ActiveRecord::Tasks::DatabaseTasks.create_current(
- ActiveSupport::StringInquirer.new("development")
- )
- end
+ ActiveRecord::Base.connection_handler.stub(:establish_connection, nil) do
+ yield
+ end
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
end
class DatabaseTasksCreateCurrentThreeTierTest < ActiveRecord::TestCase
@@ -246,80 +352,112 @@ module ActiveRecord
@configurations = {
"development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } },
"test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } },
- "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } }
+ "production" => { "primary" => { "url" => "abstract://prod-db-host/prod-db" }, "secondary" => { "url" => "abstract://secondary-prod-db-host/secondary-prod-db" } }
}
-
- ActiveRecord::Base.stubs(:configurations).returns(@configurations)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
end
def test_creates_current_environment_database
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "test-db")
-
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "secondary-test-db")
-
- ActiveRecord::Tasks::DatabaseTasks.create_current(
- ActiveSupport::StringInquirer.new("test")
- )
+ with_stubbed_configurations_establish_connection do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ [
+ ["database" => "test-db"],
+ ["database" => "secondary-test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("test")
+ )
+ end
+ end
end
def test_creates_current_environment_database_with_url
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("url" => "prod-db-url")
-
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("url" => "secondary-prod-db-url")
-
- ActiveRecord::Tasks::DatabaseTasks.create_current(
- ActiveSupport::StringInquirer.new("production")
- )
+ with_stubbed_configurations_establish_connection do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ [
+ ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"],
+ ["adapter" => "abstract", "database" => "secondary-prod-db", "host" => "secondary-prod-db-host"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("production")
+ )
+ end
+ end
end
def test_creates_test_and_development_databases_when_env_was_not_specified
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "dev-db")
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "secondary-dev-db")
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "test-db")
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "secondary-test-db")
-
- ActiveRecord::Tasks::DatabaseTasks.create_current(
- ActiveSupport::StringInquirer.new("development")
- )
+ with_stubbed_configurations_establish_connection do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ [
+ ["database" => "dev-db"],
+ ["database" => "secondary-dev-db"],
+ ["database" => "test-db"],
+ ["database" => "secondary-test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
end
def test_creates_test_and_development_databases_when_rails_env_is_development
old_env = ENV["RAILS_ENV"]
ENV["RAILS_ENV"] = "development"
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "dev-db")
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "secondary-dev-db")
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "test-db")
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "secondary-test-db")
-
- ActiveRecord::Tasks::DatabaseTasks.create_current(
- ActiveSupport::StringInquirer.new("development")
- )
+
+ with_stubbed_configurations_establish_connection do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ [
+ ["database" => "dev-db"],
+ ["database" => "secondary-dev-db"],
+ ["database" => "test-db"],
+ ["database" => "secondary-test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
ensure
ENV["RAILS_ENV"] = old_env
end
def test_establishes_connection_for_the_given_environments_config
- ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true
+ ActiveRecord::Tasks::DatabaseTasks.stub(:create, nil) do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [:development]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
+ end
- ActiveRecord::Base.expects(:establish_connection).with(:development)
+ private
+ def with_stubbed_configurations_establish_connection
+ old_configurations = ActiveRecord::Base.configurations
+ ActiveRecord::Base.configurations = @configurations
- ActiveRecord::Tasks::DatabaseTasks.create_current(
- ActiveSupport::StringInquirer.new("development")
- )
- end
+ ActiveRecord::Base.connection_handler.stub(:establish_connection, nil) do
+ yield
+ end
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
end
class DatabaseTasksDropTest < ActiveRecord::TestCase
@@ -327,8 +465,11 @@ module ActiveRecord
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_drop") do
- eval("@#{v}").expects(:drop)
- ActiveRecord::Tasks::DatabaseTasks.drop "adapter" => k
+ with_stubbed_new do
+ assert_called(eval("@#{v}"), :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop "adapter" => k
+ end
+ end
end
end
end
@@ -337,57 +478,84 @@ module ActiveRecord
def setup
@configurations = { development: { "database" => "my-db" } }
- ActiveRecord::Base.stubs(:configurations).returns(@configurations)
+ $stdout, @original_stdout = StringIO.new, $stdout
+ $stderr, @original_stderr = StringIO.new, $stderr
end
- def test_ignores_configurations_without_databases
- @configurations[:development].merge!("database" => nil)
+ def teardown
+ $stdout, $stderr = @original_stdout, @original_stderr
+ end
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).never
+ def test_ignores_configurations_without_databases
+ @configurations[:development]["database"] = nil
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ with_stubbed_configurations do
+ assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
+ end
end
def test_ignores_remote_databases
- @configurations[:development].merge!("host" => "my.server.tld")
- $stderr.stubs(:puts).returns(nil)
+ @configurations[:development]["host"] = "my.server.tld"
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).never
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ with_stubbed_configurations do
+ assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
+ end
end
def test_warning_for_remote_databases
- @configurations[:development].merge!("host" => "my.server.tld")
+ @configurations[:development]["host"] = "my.server.tld"
- $stderr.expects(:puts).with("This task only modifies local databases. my-db is on a remote host.")
+ with_stubbed_configurations do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ assert_match "This task only modifies local databases. my-db is on a remote host.",
+ $stderr.string
+ end
end
def test_drops_configurations_with_local_ip
- @configurations[:development].merge!("host" => "127.0.0.1")
+ @configurations[:development]["host"] = "127.0.0.1"
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ with_stubbed_configurations do
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
+ end
end
def test_drops_configurations_with_local_host
- @configurations[:development].merge!("host" => "localhost")
-
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
+ @configurations[:development]["host"] = "localhost"
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ with_stubbed_configurations do
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
+ end
end
def test_drops_configurations_with_blank_hosts
- @configurations[:development].merge!("host" => nil)
+ @configurations[:development]["host"] = nil
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ with_stubbed_configurations do
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
+ end
end
+
+ private
+ def with_stubbed_configurations
+ old_configurations = ActiveRecord::Base.configurations
+ ActiveRecord::Base.configurations = @configurations
+
+ yield
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
end
class DatabaseTasksDropCurrentTest < ActiveRecord::TestCase
@@ -395,55 +563,80 @@ module ActiveRecord
@configurations = {
"development" => { "database" => "dev-db" },
"test" => { "database" => "test-db" },
- "production" => { "url" => "prod-db-url" }
+ "production" => { "url" => "abstract://prod-db-host/prod-db" }
}
-
- ActiveRecord::Base.stubs(:configurations).returns(@configurations)
end
def test_drops_current_environment_database
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "test-db")
-
- ActiveRecord::Tasks::DatabaseTasks.drop_current(
- ActiveSupport::StringInquirer.new("test")
- )
+ with_stubbed_configurations do
+ assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop,
+ ["database" => "test-db"]) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("test")
+ )
+ end
+ end
end
def test_drops_current_environment_database_with_url
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("url" => "prod-db-url")
-
- ActiveRecord::Tasks::DatabaseTasks.drop_current(
- ActiveSupport::StringInquirer.new("production")
- )
+ with_stubbed_configurations do
+ assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop,
+ ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"]) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("production")
+ )
+ end
+ end
end
def test_drops_test_and_development_databases_when_env_was_not_specified
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "dev-db")
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "test-db")
-
- ActiveRecord::Tasks::DatabaseTasks.drop_current(
- ActiveSupport::StringInquirer.new("development")
- )
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :drop,
+ [
+ ["database" => "dev-db"],
+ ["database" => "test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
end
def test_drops_testand_development_databases_when_rails_env_is_development
old_env = ENV["RAILS_ENV"]
ENV["RAILS_ENV"] = "development"
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "dev-db")
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "test-db")
- ActiveRecord::Tasks::DatabaseTasks.drop_current(
- ActiveSupport::StringInquirer.new("development")
- )
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :drop,
+ [
+ ["database" => "dev-db"],
+ ["database" => "test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
ensure
ENV["RAILS_ENV"] = old_env
end
+
+ private
+ def with_stubbed_configurations
+ old_configurations = ActiveRecord::Base.configurations
+ ActiveRecord::Base.configurations = @configurations
+
+ yield
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
end
class DatabaseTasksDropCurrentThreeTierTest < ActiveRecord::TestCase
@@ -451,73 +644,100 @@ module ActiveRecord
@configurations = {
"development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } },
"test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } },
- "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } }
+ "production" => { "primary" => { "url" => "abstract://prod-db-host/prod-db" }, "secondary" => { "url" => "abstract://secondary-prod-db-host/secondary-prod-db" } }
}
-
- ActiveRecord::Base.stubs(:configurations).returns(@configurations)
end
def test_drops_current_environment_database
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "test-db")
-
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "secondary-test-db")
-
- ActiveRecord::Tasks::DatabaseTasks.drop_current(
- ActiveSupport::StringInquirer.new("test")
- )
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :drop,
+ [
+ ["database" => "test-db"],
+ ["database" => "secondary-test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("test")
+ )
+ end
+ end
end
def test_drops_current_environment_database_with_url
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("url" => "prod-db-url")
-
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("url" => "secondary-prod-db-url")
-
- ActiveRecord::Tasks::DatabaseTasks.drop_current(
- ActiveSupport::StringInquirer.new("production")
- )
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :drop,
+ [
+ ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"],
+ ["adapter" => "abstract", "database" => "secondary-prod-db", "host" => "secondary-prod-db-host"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("production")
+ )
+ end
+ end
end
def test_drops_test_and_development_databases_when_env_was_not_specified
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "dev-db")
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "secondary-dev-db")
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "test-db")
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "secondary-test-db")
-
- ActiveRecord::Tasks::DatabaseTasks.drop_current(
- ActiveSupport::StringInquirer.new("development")
- )
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :drop,
+ [
+ ["database" => "dev-db"],
+ ["database" => "secondary-dev-db"],
+ ["database" => "test-db"],
+ ["database" => "secondary-test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
end
def test_drops_testand_development_databases_when_rails_env_is_development
old_env = ENV["RAILS_ENV"]
ENV["RAILS_ENV"] = "development"
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "dev-db")
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "secondary-dev-db")
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "test-db")
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "secondary-test-db")
-
- ActiveRecord::Tasks::DatabaseTasks.drop_current(
- ActiveSupport::StringInquirer.new("development")
- )
+
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :drop,
+ [
+ ["database" => "dev-db"],
+ ["database" => "secondary-dev-db"],
+ ["database" => "test-db"],
+ ["database" => "secondary-test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
ensure
ENV["RAILS_ENV"] = old_env
end
+
+ private
+ def with_stubbed_configurations
+ old_configurations = ActiveRecord::Base.configurations
+ ActiveRecord::Base.configurations = @configurations
+
+ yield
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
end
if current_adapter?(:SQLite3Adapter) && !in_memory_db?
- class DatabaseTasksMigrateTest < ActiveRecord::TestCase
+ class DatabaseTasksMigrationTestCase < ActiveRecord::TestCase
self.use_transactional_tests = false
# Use a memory db here to avoid having to rollback at the end
@@ -537,7 +757,9 @@ module ActiveRecord
@conn.release_connection if @conn
ActiveRecord::Base.establish_connection :arunit
end
+ end
+ class DatabaseTasksMigrateTest < DatabaseTasksMigrationTestCase
def test_migrate_set_and_unset_verbose_and_version_env_vars
verbose, version = ENV["VERBOSE"], ENV["VERSION"]
ENV["VERSION"] = "2"
@@ -598,6 +820,26 @@ module ActiveRecord
end
end
end
+
+ class DatabaseTasksMigrateStatusTest < DatabaseTasksMigrationTestCase
+ def test_migrate_status_table
+ ActiveRecord::SchemaMigration.create_table
+ output = capture_migration_status
+ assert_match(/database: :memory:/, output)
+ assert_match(/down 001 Valid people have last names/, output)
+ assert_match(/down 002 We need reminders/, output)
+ assert_match(/down 003 Innocent jointable/, output)
+ ActiveRecord::SchemaMigration.drop_table
+ end
+
+ private
+
+ def capture_migration_status
+ capture(:stdout) do
+ ActiveRecord::Tasks::DatabaseTasks.migrate_status
+ end
+ end
+ end
end
class DatabaseTasksMigrateErrorTest < ActiveRecord::TestCase
@@ -638,15 +880,16 @@ module ActiveRecord
end
def test_migrate_raise_error_on_failed_check_target_version
- ActiveRecord::Tasks::DatabaseTasks.stubs(:check_target_version).raises("foo")
-
- e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
- assert_equal "foo", e.message
+ ActiveRecord::Tasks::DatabaseTasks.stub(:check_target_version, -> { raise "foo" }) do
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_equal "foo", e.message
+ end
end
def test_migrate_clears_schema_cache_afterward
- ActiveRecord::Base.expects(:clear_cache!)
- ActiveRecord::Tasks::DatabaseTasks.migrate
+ assert_called(ActiveRecord::Base, :clear_cache!) do
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
end
end
@@ -655,48 +898,238 @@ module ActiveRecord
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_purge") do
- eval("@#{v}").expects(:purge)
- ActiveRecord::Tasks::DatabaseTasks.purge "adapter" => k
+ with_stubbed_new do
+ assert_called(eval("@#{v}"), :purge) do
+ ActiveRecord::Tasks::DatabaseTasks.purge "adapter" => k
+ end
+ end
end
end
end
class DatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase
def test_purges_current_environment_database
+ old_configurations = ActiveRecord::Base.configurations
configurations = {
"development" => { "database" => "dev-db" },
"test" => { "database" => "test-db" },
"production" => { "database" => "prod-db" }
}
- ActiveRecord::Base.stubs(:configurations).returns(configurations)
- ActiveRecord::Tasks::DatabaseTasks.expects(:purge).
- with("database" => "prod-db")
- ActiveRecord::Base.expects(:establish_connection).with(:production)
+ ActiveRecord::Base.configurations = configurations
- ActiveRecord::Tasks::DatabaseTasks.purge_current("production")
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :purge,
+ ["database" => "prod-db"]
+ ) do
+ assert_called_with(ActiveRecord::Base, :establish_connection, [:production]) do
+ ActiveRecord::Tasks::DatabaseTasks.purge_current("production")
+ end
+ end
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
end
end
class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase
def test_purge_all_local_configurations
+ old_configurations = ActiveRecord::Base.configurations
configurations = { development: { "database" => "my-db" } }
- ActiveRecord::Base.stubs(:configurations).returns(configurations)
+ ActiveRecord::Base.configurations = configurations
+
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :purge,
+ ["database" => "my-db"]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.purge_all
+ end
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
+ end
+
+ unless in_memory_db?
+ class DatabaseTasksTruncateAllTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ fixtures :authors, :author_addresses
+
+ def setup
+ SchemaMigration.create_table
+ SchemaMigration.create!(version: SchemaMigration.table_name)
+ InternalMetadata.create_table
+ InternalMetadata.create!(key: InternalMetadata.table_name)
+ end
+
+ def teardown
+ SchemaMigration.delete_all
+ InternalMetadata.delete_all
+ ActiveRecord::Base.connection_handlers = { writing: ActiveRecord::Base.default_connection_handler }
+ end
- ActiveRecord::Tasks::DatabaseTasks.expects(:purge).
- with("database" => "my-db")
+ def test_truncate_tables
+ assert_operator SchemaMigration.count, :>, 0
+ assert_operator InternalMetadata.count, :>, 0
+ assert_operator Author.count, :>, 0
+ assert_operator AuthorAddress.count, :>, 0
- ActiveRecord::Tasks::DatabaseTasks.purge_all
+ old_configurations = ActiveRecord::Base.configurations
+ configurations = { development: ActiveRecord::Base.configurations["arunit"] }
+ ActiveRecord::Base.configurations = configurations
+
+ ActiveRecord::Tasks::DatabaseTasks.stub(:root, nil) do
+ ActiveRecord::Tasks::DatabaseTasks.truncate_all(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+
+ assert_operator SchemaMigration.count, :>, 0
+ assert_operator InternalMetadata.count, :>, 0
+ assert_equal 0, Author.count
+ assert_equal 0, AuthorAddress.count
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
+ end
+
+ class DatabaseTasksTruncateAllWithPrefixTest < DatabaseTasksTruncateAllTest
+ setup do
+ ActiveRecord::Base.table_name_prefix = "p_"
+
+ SchemaMigration.reset_table_name
+ InternalMetadata.reset_table_name
+ end
+
+ teardown do
+ ActiveRecord::Base.table_name_prefix = nil
+
+ SchemaMigration.reset_table_name
+ InternalMetadata.reset_table_name
+ end
+ end
+
+ class DatabaseTasksTruncateAllWithSuffixTest < DatabaseTasksTruncateAllTest
+ setup do
+ ActiveRecord::Base.table_name_suffix = "_s"
+
+ SchemaMigration.reset_table_name
+ InternalMetadata.reset_table_name
+ end
+
+ teardown do
+ ActiveRecord::Base.table_name_suffix = nil
+
+ SchemaMigration.reset_table_name
+ InternalMetadata.reset_table_name
+ end
end
end
+ class DatabaseTasksTruncateAllWithMultipleDatabasesTest < ActiveRecord::TestCase
+ def setup
+ @configurations = {
+ "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } },
+ "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } },
+ "production" => { "primary" => { "url" => "abstract://prod-db-host/prod-db" }, "secondary" => { "url" => "abstract://secondary-prod-db-host/secondary-prod-db" } }
+ }
+ end
+
+ def test_truncate_all_databases_for_environment
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :truncate_tables,
+ [
+ ["database" => "test-db"],
+ ["database" => "secondary-test-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.truncate_all(
+ ActiveSupport::StringInquirer.new("test")
+ )
+ end
+ end
+ end
+
+ def test_truncate_all_databases_with_url_for_environment
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :truncate_tables,
+ [
+ ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"],
+ ["adapter" => "abstract", "database" => "secondary-prod-db", "host" => "secondary-prod-db-host"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.truncate_all(
+ ActiveSupport::StringInquirer.new("production")
+ )
+ end
+ end
+ end
+
+ def test_truncate_all_development_databases_when_env_is_not_specified
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :truncate_tables,
+ [
+ ["database" => "dev-db"],
+ ["database" => "secondary-dev-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.truncate_all(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
+ end
+
+ def test_truncate_all_development_databases_when_env_is_development
+ old_env = ENV["RAILS_ENV"]
+ ENV["RAILS_ENV"] = "development"
+
+ with_stubbed_configurations do
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :truncate_tables,
+ [
+ ["database" => "dev-db"],
+ ["database" => "secondary-dev-db"]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.truncate_all(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
+ ensure
+ ENV["RAILS_ENV"] = old_env
+ end
+
+ private
+ def with_stubbed_configurations
+ old_configurations = ActiveRecord::Base.configurations
+ ActiveRecord::Base.configurations = @configurations
+
+ yield
+ ensure
+ ActiveRecord::Base.configurations = old_configurations
+ end
+ end
+
class DatabaseTasksCharsetTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_charset") do
- eval("@#{v}").expects(:charset)
- ActiveRecord::Tasks::DatabaseTasks.charset "adapter" => k
+ with_stubbed_new do
+ assert_called(eval("@#{v}"), :charset) do
+ ActiveRecord::Tasks::DatabaseTasks.charset "adapter" => k
+ end
+ end
end
end
end
@@ -706,8 +1139,11 @@ module ActiveRecord
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_collation") do
- eval("@#{v}").expects(:collation)
- ActiveRecord::Tasks::DatabaseTasks.collation "adapter" => k
+ with_stubbed_new do
+ assert_called(eval("@#{v}"), :collation) do
+ ActiveRecord::Tasks::DatabaseTasks.collation "adapter" => k
+ end
+ end
end
end
end
@@ -819,8 +1255,14 @@ module ActiveRecord
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_structure_dump") do
- eval("@#{v}").expects(:structure_dump).with("awesome-file.sql", nil)
- ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => k }, "awesome-file.sql")
+ with_stubbed_new do
+ assert_called_with(
+ eval("@#{v}"), :structure_dump,
+ ["awesome-file.sql", nil]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => k }, "awesome-file.sql")
+ end
+ end
end
end
end
@@ -830,31 +1272,41 @@ module ActiveRecord
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_structure_load") do
- eval("@#{v}").expects(:structure_load).with("awesome-file.sql", nil)
- ActiveRecord::Tasks::DatabaseTasks.structure_load({ "adapter" => k }, "awesome-file.sql")
+ with_stubbed_new do
+ assert_called_with(
+ eval("@#{v}"),
+ :structure_load,
+ ["awesome-file.sql", nil]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_load({ "adapter" => k }, "awesome-file.sql")
+ end
+ end
end
end
end
class DatabaseTasksCheckSchemaFileTest < ActiveRecord::TestCase
def test_check_schema_file
- Kernel.expects(:abort).with(regexp_matches(/awesome-file.sql/))
- ActiveRecord::Tasks::DatabaseTasks.check_schema_file("awesome-file.sql")
+ assert_called_with(Kernel, :abort, [/awesome-file.sql/]) do
+ ActiveRecord::Tasks::DatabaseTasks.check_schema_file("awesome-file.sql")
+ end
end
end
class DatabaseTasksCheckSchemaFileDefaultsTest < ActiveRecord::TestCase
def test_check_schema_file_defaults
- ActiveRecord::Tasks::DatabaseTasks.stubs(:db_dir).returns("/tmp")
- assert_equal "/tmp/schema.rb", ActiveRecord::Tasks::DatabaseTasks.schema_file
+ ActiveRecord::Tasks::DatabaseTasks.stub(:db_dir, "/tmp") do
+ assert_equal "/tmp/schema.rb", ActiveRecord::Tasks::DatabaseTasks.schema_file
+ end
end
end
class DatabaseTasksCheckSchemaFileSpecifiedFormatsTest < ActiveRecord::TestCase
{ ruby: "schema.rb", sql: "structure.sql" }.each_pair do |fmt, filename|
define_method("test_check_schema_file_for_#{fmt}_format") do
- ActiveRecord::Tasks::DatabaseTasks.stubs(:db_dir).returns("/tmp")
- assert_equal "/tmp/#{filename}", ActiveRecord::Tasks::DatabaseTasks.schema_file(fmt)
+ ActiveRecord::Tasks::DatabaseTasks.stub(:db_dir, "/tmp") do
+ assert_equal "/tmp/#{filename}", ActiveRecord::Tasks::DatabaseTasks.schema_file(fmt)
+ end
end
end
end
diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb
index 047153e7cc..552e623fd4 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -7,15 +7,11 @@ if current_adapter?(:Mysql2Adapter)
module ActiveRecord
class MysqlDBCreateTest < ActiveRecord::TestCase
def setup
- @connection = stub(create_database: true)
+ @connection = Class.new { def create_database(*); end }.new
@configuration = {
"adapter" => "mysql2",
"database" => "my-app-db"
}
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
-
$stdout, @original_stdout = StringIO.new, $stdout
$stderr, @original_stderr = StringIO.new, $stderr
end
@@ -25,59 +21,97 @@ if current_adapter?(:Mysql2Adapter)
end
def test_establishes_connection_without_database
- ActiveRecord::Base.expects(:establish_connection).
- with("adapter" => "mysql2", "database" => nil)
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [
+ [ "adapter" => "mysql2", "database" => nil ],
+ [ "adapter" => "mysql2", "database" => "my-app-db" ],
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ end
end
def test_creates_database_with_no_default_options
- @connection.expects(:create_database).
- with("my-app-db", {})
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ with_stubbed_connection_establish_connection do
+ assert_called_with(@connection, :create_database, ["my-app-db", {}]) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ end
end
def test_creates_database_with_given_encoding
- @connection.expects(:create_database).
- with("my-app-db", charset: "latin1")
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge("encoding" => "latin1")
+ with_stubbed_connection_establish_connection do
+ assert_called_with(@connection, :create_database, ["my-app-db", charset: "latin1"]) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge("encoding" => "latin1")
+ end
+ end
end
def test_creates_database_with_given_collation
- @connection.expects(:create_database).
- with("my-app-db", collation: "latin1_swedish_ci")
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge("collation" => "latin1_swedish_ci")
+ with_stubbed_connection_establish_connection do
+ assert_called_with(
+ @connection,
+ :create_database,
+ ["my-app-db", collation: "latin1_swedish_ci"]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration.merge("collation" => "latin1_swedish_ci")
+ end
+ end
end
def test_establishes_connection_to_database
- ActiveRecord::Base.expects(:establish_connection).with(@configuration)
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [
+ ["adapter" => "mysql2", "database" => nil],
+ [@configuration]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ end
end
def test_when_database_created_successfully_outputs_info_to_stdout
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ with_stubbed_connection_establish_connection do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
- assert_equal "Created database 'my-app-db'\n", $stdout.string
+ assert_equal "Created database 'my-app-db'\n", $stdout.string
+ end
end
def test_create_when_database_exists_outputs_info_to_stderr
- ActiveRecord::Base.connection.stubs(:create_database).raises(
- ActiveRecord::Tasks::DatabaseAlreadyExists
- )
+ with_stubbed_connection_establish_connection do
+ ActiveRecord::Base.connection.stub(
+ :create_database,
+ proc { raise ActiveRecord::Tasks::DatabaseAlreadyExists }
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+
+ assert_equal "Database 'my-app-db' already exists\n", $stderr.string
+ end
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ private
- assert_equal "Database 'my-app-db' already exists\n", $stderr.string
- end
+ def with_stubbed_connection_establish_connection
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ ActiveRecord::Base.stub(:connection, @connection) do
+ yield
+ end
+ end
+ end
end
class MysqlDBCreateWithInvalidPermissionsTest < ActiveRecord::TestCase
def setup
- @connection = stub("Connection", create_database: true)
@error = Mysql2::Error.new("Invalid permissions")
@configuration = {
"adapter" => "mysql2",
@@ -85,10 +119,6 @@ if current_adapter?(:Mysql2Adapter)
"username" => "pat",
"password" => "wossname"
}
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).raises(@error)
-
$stdout, @original_stdout = StringIO.new, $stdout
$stderr, @original_stderr = StringIO.new, $stderr
end
@@ -98,23 +128,21 @@ if current_adapter?(:Mysql2Adapter)
end
def test_raises_error
- assert_raises(Mysql2::Error) do
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ ActiveRecord::Base.stub(:establish_connection, -> * { raise @error }) do
+ assert_raises(Mysql2::Error, "Invalid permissions") do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
end
end
end
class MySQLDBDropTest < ActiveRecord::TestCase
def setup
- @connection = stub(drop_database: true)
+ @connection = Class.new { def drop_database(name); end }.new
@configuration = {
"adapter" => "mysql2",
"database" => "my-app-db"
}
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
-
$stdout, @original_stdout = StringIO.new, $stdout
$stderr, @original_stderr = StringIO.new, $stderr
end
@@ -124,91 +152,130 @@ if current_adapter?(:Mysql2Adapter)
end
def test_establishes_connection_to_mysql_database
- ActiveRecord::Base.expects(:establish_connection).with @configuration
-
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [@configuration]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+ end
+ end
end
def test_drops_database
- @connection.expects(:drop_database).with("my-app-db")
-
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+ with_stubbed_connection_establish_connection do
+ assert_called_with(@connection, :drop_database, ["my-app-db"]) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+ end
+ end
end
def test_when_database_dropped_successfully_outputs_info_to_stdout
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+ with_stubbed_connection_establish_connection do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration
- assert_equal "Dropped database 'my-app-db'\n", $stdout.string
+ assert_equal "Dropped database 'my-app-db'\n", $stdout.string
+ end
end
+
+ private
+
+ def with_stubbed_connection_establish_connection
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ ActiveRecord::Base.stub(:connection, @connection) do
+ yield
+ end
+ end
+ end
end
class MySQLPurgeTest < ActiveRecord::TestCase
def setup
- @connection = stub(recreate_database: true)
+ @connection = Class.new { def recreate_database(*); end }.new
@configuration = {
"adapter" => "mysql2",
"database" => "test-db"
}
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
end
def test_establishes_connection_to_the_appropriate_database
- ActiveRecord::Base.expects(:establish_connection).with(@configuration)
-
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [@configuration]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ end
+ end
end
def test_recreates_database_with_no_default_options
- @connection.expects(:recreate_database).
- with("test-db", {})
-
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ with_stubbed_connection_establish_connection do
+ assert_called_with(@connection, :recreate_database, ["test-db", {}]) do
+ ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ end
+ end
end
def test_recreates_database_with_the_given_options
- @connection.expects(:recreate_database).
- with("test-db", charset: "latin", collation: "latin1_swedish_ci")
-
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration.merge(
- "encoding" => "latin", "collation" => "latin1_swedish_ci")
+ with_stubbed_connection_establish_connection do
+ assert_called_with(
+ @connection,
+ :recreate_database,
+ ["test-db", charset: "latin", collation: "latin1_swedish_ci"]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.purge @configuration.merge(
+ "encoding" => "latin", "collation" => "latin1_swedish_ci")
+ end
+ end
end
+
+ private
+
+ def with_stubbed_connection_establish_connection
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ ActiveRecord::Base.stub(:connection, @connection) do
+ yield
+ end
+ end
+ end
end
class MysqlDBCharsetTest < ActiveRecord::TestCase
def setup
- @connection = stub(create_database: true)
+ @connection = Class.new { def charset; end }.new
@configuration = {
"adapter" => "mysql2",
"database" => "my-app-db"
}
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
end
def test_db_retrieves_charset
- @connection.expects(:charset)
- ActiveRecord::Tasks::DatabaseTasks.charset @configuration
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called(@connection, :charset) do
+ ActiveRecord::Tasks::DatabaseTasks.charset @configuration
+ end
+ end
end
end
class MysqlDBCollationTest < ActiveRecord::TestCase
def setup
- @connection = stub(create_database: true)
+ @connection = Class.new { def collation; end }.new
@configuration = {
"adapter" => "mysql2",
"database" => "my-app-db"
}
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
end
def test_db_retrieves_collation
- @connection.expects(:collation)
- ActiveRecord::Tasks::DatabaseTasks.collation @configuration
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called(@connection, :collation) do
+ ActiveRecord::Tasks::DatabaseTasks.collation @configuration
+ end
+ end
end
end
@@ -222,9 +289,14 @@ if current_adapter?(:Mysql2Adapter)
def test_structure_dump
filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db").returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ end
end
def test_structure_dump_with_extra_flags
@@ -240,41 +312,59 @@ if current_adapter?(:Mysql2Adapter)
def test_structure_dump_with_ignore_tables
filename = "awesome-file.sql"
- ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo", "bar"])
-
- Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--ignore-table=test-db.foo", "--ignore-table=test-db.bar", "test-db").returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ ActiveRecord::SchemaDumper.stub(:ignore_tables, ["foo", "bar"]) do
+ assert_called_with(
+ Kernel,
+ :system,
+ ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--ignore-table=test-db.foo", "--ignore-table=test-db.bar", "test-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ end
+ end
end
def test_warn_when_external_structure_dump_command_execution_fails
filename = "awesome-file.sql"
- Kernel.expects(:system)
- .with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db")
- .returns(false)
-
- e = assert_raise(RuntimeError) {
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
- }
- assert_match(/^failed to execute: `mysqldump`$/, e.message)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"],
+ returns: false
+ ) do
+ e = assert_raise(RuntimeError) {
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ }
+ assert_match(/^failed to execute: `mysqldump`$/, e.message)
+ end
end
def test_structure_dump_with_port_number
filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--port=10000", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db").returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(
- @configuration.merge("port" => 10000),
- filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["mysqldump", "--port=10000", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(
+ @configuration.merge("port" => 10000),
+ filename)
+ end
end
def test_structure_dump_with_ssl
filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--ssl-ca=ca.crt", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db").returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(
- @configuration.merge("sslca" => "ca.crt"),
- filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["mysqldump", "--ssl-ca=ca.crt", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(
+ @configuration.merge("sslca" => "ca.crt"),
+ filename)
+ end
end
private
diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb
index ca1defa332..065ba7734c 100644
--- a/activerecord/test/cases/tasks/postgresql_rake_test.rb
+++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb
@@ -7,15 +7,11 @@ if current_adapter?(:PostgreSQLAdapter)
module ActiveRecord
class PostgreSQLDBCreateTest < ActiveRecord::TestCase
def setup
- @connection = stub(create_database: true)
+ @connection = Class.new { def create_database(*); end }.new
@configuration = {
"adapter" => "postgresql",
"database" => "my-app-db"
}
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
-
$stdout, @original_stdout = StringIO.new, $stdout
$stderr, @original_stderr = StringIO.new, $stderr
end
@@ -25,82 +21,141 @@ if current_adapter?(:PostgreSQLAdapter)
end
def test_establishes_connection_to_postgresql_database
- ActiveRecord::Base.expects(:establish_connection).with(
- "adapter" => "postgresql",
- "database" => "postgres",
- "schema_search_path" => "public"
- )
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [
+ [
+ "adapter" => "postgresql",
+ "database" => "postgres",
+ "schema_search_path" => "public"
+ ],
+ [
+ "adapter" => "postgresql",
+ "database" => "my-app-db"
+ ]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ end
end
def test_creates_database_with_default_encoding
- @connection.expects(:create_database).
- with("my-app-db", @configuration.merge("encoding" => "utf8"))
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ with_stubbed_connection_establish_connection do
+ assert_called_with(
+ @connection,
+ :create_database,
+ ["my-app-db", @configuration.merge("encoding" => "utf8")]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ end
end
def test_creates_database_with_given_encoding
- @connection.expects(:create_database).
- with("my-app-db", @configuration.merge("encoding" => "latin"))
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration.
- merge("encoding" => "latin")
+ with_stubbed_connection_establish_connection do
+ assert_called_with(
+ @connection,
+ :create_database,
+ ["my-app-db", @configuration.merge("encoding" => "latin")]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration.
+ merge("encoding" => "latin")
+ end
+ end
end
def test_creates_database_with_given_collation_and_ctype
- @connection.expects(:create_database).
- with("my-app-db", @configuration.merge("encoding" => "utf8", "collation" => "ja_JP.UTF8", "ctype" => "ja_JP.UTF8"))
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration.
- merge("collation" => "ja_JP.UTF8", "ctype" => "ja_JP.UTF8")
+ with_stubbed_connection_establish_connection do
+ assert_called_with(
+ @connection,
+ :create_database,
+ [
+ "my-app-db",
+ @configuration.merge(
+ "encoding" => "utf8",
+ "collation" => "ja_JP.UTF8",
+ "ctype" => "ja_JP.UTF8"
+ )
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration.
+ merge("collation" => "ja_JP.UTF8", "ctype" => "ja_JP.UTF8")
+ end
+ end
end
def test_establishes_connection_to_new_database
- ActiveRecord::Base.expects(:establish_connection).with(@configuration)
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [
+ [
+ "adapter" => "postgresql",
+ "database" => "postgres",
+ "schema_search_path" => "public"
+ ],
+ [
+ @configuration
+ ]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ end
end
def test_db_create_with_error_prints_message
- ActiveRecord::Base.stubs(:establish_connection).raises(Exception)
-
- $stderr.stubs(:puts).returns(true)
- $stderr.expects(:puts).
- with("Couldn't create database for #{@configuration.inspect}")
-
- assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration }
+ ActiveRecord::Base.stub(:connection, @connection) do
+ ActiveRecord::Base.stub(:establish_connection, -> * { raise Exception }) do
+ assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration }
+ assert_match "Couldn't create '#{@configuration['database']}' database. Please check your configuration.", $stderr.string
+ end
+ end
end
def test_when_database_created_successfully_outputs_info_to_stdout
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ with_stubbed_connection_establish_connection do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
- assert_equal "Created database 'my-app-db'\n", $stdout.string
+ assert_equal "Created database 'my-app-db'\n", $stdout.string
+ end
end
def test_create_when_database_exists_outputs_info_to_stderr
- ActiveRecord::Base.connection.stubs(:create_database).raises(
- ActiveRecord::Tasks::DatabaseAlreadyExists
- )
+ with_stubbed_connection_establish_connection do
+ ActiveRecord::Base.connection.stub(
+ :create_database,
+ proc { raise ActiveRecord::Tasks::DatabaseAlreadyExists }
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+
+ assert_equal "Database 'my-app-db' already exists\n", $stderr.string
+ end
+ end
+ end
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ private
- assert_equal "Database 'my-app-db' already exists\n", $stderr.string
- end
+ def with_stubbed_connection_establish_connection
+ ActiveRecord::Base.stub(:connection, @connection) do
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ yield
+ end
+ end
+ end
end
class PostgreSQLDBDropTest < ActiveRecord::TestCase
def setup
- @connection = stub(drop_database: true)
+ @connection = Class.new { def drop_database(*); end }.new
@configuration = {
"adapter" => "postgresql",
"database" => "my-app-db"
}
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
-
$stdout, @original_stdout = StringIO.new, $stdout
$stderr, @original_stderr = StringIO.new, $stderr
end
@@ -110,125 +165,197 @@ if current_adapter?(:PostgreSQLAdapter)
end
def test_establishes_connection_to_postgresql_database
- ActiveRecord::Base.expects(:establish_connection).with(
- "adapter" => "postgresql",
- "database" => "postgres",
- "schema_search_path" => "public"
- )
-
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [
+ "adapter" => "postgresql",
+ "database" => "postgres",
+ "schema_search_path" => "public"
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+ end
+ end
end
def test_drops_database
- @connection.expects(:drop_database).with("my-app-db")
-
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+ with_stubbed_connection_establish_connection do
+ assert_called_with(
+ @connection,
+ :drop_database,
+ ["my-app-db"]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+ end
+ end
end
def test_when_database_dropped_successfully_outputs_info_to_stdout
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration
+ with_stubbed_connection_establish_connection do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration
- assert_equal "Dropped database 'my-app-db'\n", $stdout.string
+ assert_equal "Dropped database 'my-app-db'\n", $stdout.string
+ end
end
+
+ private
+
+ def with_stubbed_connection_establish_connection
+ ActiveRecord::Base.stub(:connection, @connection) do
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ yield
+ end
+ end
+ end
end
class PostgreSQLPurgeTest < ActiveRecord::TestCase
def setup
- @connection = stub(create_database: true, drop_database: true)
+ @connection = Class.new do
+ def create_database(*); end
+ def drop_database(*); end
+ end.new
@configuration = {
"adapter" => "postgresql",
"database" => "my-app-db"
}
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:clear_active_connections!).returns(true)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
end
def test_clears_active_connections
- ActiveRecord::Base.expects(:clear_active_connections!)
-
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ with_stubbed_connection do
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ assert_called(ActiveRecord::Base, :clear_active_connections!) do
+ ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ end
+ end
+ end
end
def test_establishes_connection_to_postgresql_database
- ActiveRecord::Base.expects(:establish_connection).with(
- "adapter" => "postgresql",
- "database" => "postgres",
- "schema_search_path" => "public"
- )
-
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ with_stubbed_connection do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [
+ [
+ "adapter" => "postgresql",
+ "database" => "postgres",
+ "schema_search_path" => "public"
+ ],
+ [
+ "adapter" => "postgresql",
+ "database" => "my-app-db"
+ ]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ end
+ end
end
def test_drops_database
- @connection.expects(:drop_database).with("my-app-db")
-
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ with_stubbed_connection do
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ assert_called_with(@connection, :drop_database, ["my-app-db"]) do
+ ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ end
+ end
+ end
end
def test_creates_database
- @connection.expects(:create_database).
- with("my-app-db", @configuration.merge("encoding" => "utf8"))
-
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ with_stubbed_connection do
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ assert_called_with(
+ @connection,
+ :create_database,
+ ["my-app-db", @configuration.merge("encoding" => "utf8")]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ end
+ end
+ end
end
def test_establishes_connection
- ActiveRecord::Base.expects(:establish_connection).with(@configuration)
-
- ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ with_stubbed_connection do
+ assert_called_with(
+ ActiveRecord::Base,
+ :establish_connection,
+ [
+ [
+ "adapter" => "postgresql",
+ "database" => "postgres",
+ "schema_search_path" => "public"
+ ],
+ [
+ @configuration
+ ]
+ ]
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.purge @configuration
+ end
+ end
end
+
+ private
+
+ def with_stubbed_connection
+ ActiveRecord::Base.stub(:connection, @connection) do
+ yield
+ end
+ end
end
class PostgreSQLDBCharsetTest < ActiveRecord::TestCase
def setup
- @connection = stub(create_database: true)
+ @connection = Class.new do
+ def create_database(*); end
+ def encoding; end
+ end.new
@configuration = {
"adapter" => "postgresql",
"database" => "my-app-db"
}
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
end
def test_db_retrieves_charset
- @connection.expects(:encoding)
- ActiveRecord::Tasks::DatabaseTasks.charset @configuration
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called(@connection, :encoding) do
+ ActiveRecord::Tasks::DatabaseTasks.charset @configuration
+ end
+ end
end
end
class PostgreSQLDBCollationTest < ActiveRecord::TestCase
def setup
- @connection = stub(create_database: true)
+ @connection = Class.new { def collation; end }.new
@configuration = {
"adapter" => "postgresql",
"database" => "my-app-db"
}
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
end
def test_db_retrieves_collation
- @connection.expects(:collation)
- ActiveRecord::Tasks::DatabaseTasks.collation @configuration
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called(@connection, :collation) do
+ ActiveRecord::Tasks::DatabaseTasks.collation @configuration
+ end
+ end
end
end
class PostgreSQLStructureDumpTest < ActiveRecord::TestCase
def setup
- @connection = stub(schema_search_path: nil, structure_dump: true)
@configuration = {
"adapter" => "postgresql",
"database" => "my-app-db"
}
@filename = "/tmp/awesome-file.sql"
FileUtils.touch(@filename)
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
end
def teardown
@@ -236,18 +363,23 @@ if current_adapter?(:PostgreSQLAdapter)
end
def test_structure_dump
- Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "my-app-db").returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "my-app-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ end
end
def test_structure_dump_header_comments_removed
- Kernel.stubs(:system).returns(true)
- File.write(@filename, "-- header comment\n\n-- more header comment\n statement \n-- lower comment\n")
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ Kernel.stub(:system, true) do
+ File.write(@filename, "-- header comment\n\n-- more header comment\n statement \n-- lower comment\n")
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
- assert_equal [" statement \n", "-- lower comment\n"], File.readlines(@filename).first(2)
+ assert_equal [" statement \n", "-- lower comment\n"], File.readlines(@filename).first(2)
+ end
end
def test_structure_dump_with_extra_flags
@@ -261,47 +393,76 @@ if current_adapter?(:PostgreSQLAdapter)
end
def test_structure_dump_with_ignore_tables
- ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo", "bar"])
-
- Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "-T", "foo", "-T", "bar", "my-app-db").returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ assert_called(
+ ActiveRecord::SchemaDumper,
+ :ignore_tables,
+ returns: ["foo", "bar"]
+ ) do
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "-T", "foo", "-T", "bar", "my-app-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ end
+ end
end
def test_structure_dump_with_schema_search_path
@configuration["schema_search_path"] = "foo,bar"
- Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db").returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db"],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ end
end
def test_structure_dump_with_schema_search_path_and_dump_schemas_all
@configuration["schema_search_path"] = "foo,bar"
- Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "my-app-db").returns(true)
-
- with_dump_schemas(:all) do
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "my-app-db"],
+ returns: true
+ ) do
+ with_dump_schemas(:all) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ end
end
end
def test_structure_dump_with_dump_schemas_string
- Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db").returns(true)
-
- with_dump_schemas("foo,bar") do
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", @filename, "--schema=foo", "--schema=bar", "my-app-db"],
+ returns: true
+ ) do
+ with_dump_schemas("foo,bar") do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename)
+ end
end
end
def test_structure_dump_execution_fails
filename = "awesome-file.sql"
- Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", filename, "my-app-db").returns(nil)
-
- e = assert_raise(RuntimeError) do
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["pg_dump", "-s", "-x", "-O", "-f", filename, "my-app-db"],
+ returns: nil
+ ) do
+ e = assert_raise(RuntimeError) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ end
+ assert_match("failed to execute:", e.message)
end
- assert_match("failed to execute:", e.message)
end
private
@@ -324,26 +485,27 @@ if current_adapter?(:PostgreSQLAdapter)
class PostgreSQLStructureLoadTest < ActiveRecord::TestCase
def setup
- @connection = stub
@configuration = {
"adapter" => "postgresql",
"database" => "my-app-db"
}
-
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
end
def test_structure_load
filename = "awesome-file.sql"
- Kernel.expects(:system).with("psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]).returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-X", "-f", filename, @configuration["database"]],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ end
end
def test_structure_load_with_extra_flags
filename = "awesome-file.sql"
- expected_command = ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, "--noop", @configuration["database"]]
+ expected_command = ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-X", "-f", filename, "--noop", @configuration["database"]]
assert_called_with(Kernel, :system, expected_command, returns: true) do
with_structure_load_flags(["--noop"]) do
@@ -354,9 +516,14 @@ if current_adapter?(:PostgreSQLAdapter)
def test_structure_load_accepts_path_with_spaces
filename = "awesome file.sql"
- Kernel.expects(:system).with("psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]).returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ assert_called_with(
+ Kernel,
+ :system,
+ ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-X", "-f", filename, @configuration["database"]],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ end
end
private
diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb
index d7e3caa2ff..c1092b97c1 100644
--- a/activerecord/test/cases/tasks/sqlite_rake_test.rb
+++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb
@@ -9,16 +9,10 @@ if current_adapter?(:SQLite3Adapter)
class SqliteDBCreateTest < ActiveRecord::TestCase
def setup
@database = "db_create.sqlite3"
- @connection = stub :connection
@configuration = {
"adapter" => "sqlite3",
"database" => @database
}
-
- File.stubs(:exist?).returns(false)
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
-
$stdout, @original_stdout = StringIO.new, $stdout
$stderr, @original_stderr = StringIO.new, $stderr
end
@@ -28,63 +22,62 @@ if current_adapter?(:SQLite3Adapter)
end
def test_db_checks_database_exists
- File.expects(:exist?).with(@database).returns(false)
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ assert_called_with(File, :exist?, [@database], returns: false) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
+ end
+ end
end
def test_when_db_created_successfully_outputs_info_to_stdout
- ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
+ ActiveRecord::Base.stub(:establish_connection, nil) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
- assert_equal "Created database '#{@database}'\n", $stdout.string
+ assert_equal "Created database '#{@database}'\n", $stdout.string
+ end
end
def test_db_create_when_file_exists
- File.stubs(:exist?).returns(true)
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
+ File.stub(:exist?, true) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
- assert_equal "Database '#{@database}' already exists\n", $stderr.string
+ assert_equal "Database '#{@database}' already exists\n", $stderr.string
+ end
end
def test_db_create_with_file_does_nothing
- File.stubs(:exist?).returns(true)
- $stderr.stubs(:puts).returns(nil)
-
- ActiveRecord::Base.expects(:establish_connection).never
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
+ File.stub(:exist?, true) do
+ assert_not_called(ActiveRecord::Base, :establish_connection) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
+ end
+ end
end
def test_db_create_establishes_a_connection
- ActiveRecord::Base.expects(:establish_connection).with(@configuration)
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
+ assert_called_with(ActiveRecord::Base, :establish_connection, [@configuration]) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root"
+ end
end
def test_db_create_with_error_prints_message
- ActiveRecord::Base.stubs(:establish_connection).raises(Exception)
-
- $stderr.stubs(:puts).returns(true)
- $stderr.expects(:puts).
- with("Couldn't create database for #{@configuration.inspect}")
-
- assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" }
+ ActiveRecord::Base.stub(:establish_connection, proc { raise Exception }) do
+ assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" }
+ assert_match "Couldn't create '#{@configuration['database']}' database. Please check your configuration.", $stderr.string
+ end
end
end
class SqliteDBDropTest < ActiveRecord::TestCase
def setup
@database = "db_create.sqlite3"
- @path = stub(to_s: "/absolute/path", absolute?: true)
@configuration = {
"adapter" => "sqlite3",
"database" => @database
}
-
- Pathname.stubs(:new).returns(@path)
- File.stubs(:join).returns("/former/relative/path")
- FileUtils.stubs(:rm).returns(true)
+ @path = Class.new do
+ def to_s; "/absolute/path" end
+ def absolute?; true end
+ end.new
$stdout, @original_stdout = StringIO.new, $stdout
$stderr, @original_stderr = StringIO.new, $stderr
@@ -95,77 +88,76 @@ if current_adapter?(:SQLite3Adapter)
end
def test_creates_path_from_database
- Pathname.expects(:new).with(@database).returns(@path)
-
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root"
+ assert_called_with(Pathname, :new, [@database], returns: @path) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root"
+ end
end
def test_removes_file_with_absolute_path
- File.stubs(:exist?).returns(true)
- @path.stubs(:absolute?).returns(true)
-
- FileUtils.expects(:rm).with("/absolute/path")
-
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root"
+ Pathname.stub(:new, @path) do
+ assert_called_with(FileUtils, :rm, ["/absolute/path"]) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root"
+ end
+ end
end
def test_generates_absolute_path_with_given_root
- @path.stubs(:absolute?).returns(false)
-
- File.expects(:join).with("/rails/root", @path).
- returns("/former/relative/path")
-
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root"
+ Pathname.stub(:new, @path) do
+ @path.stub(:absolute?, false) do
+ assert_called_with(File, :join, ["/rails/root", @path],
+ returns: "/former/relative/path"
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root"
+ end
+ end
+ end
end
def test_removes_file_with_relative_path
- File.stubs(:exist?).returns(true)
- @path.stubs(:absolute?).returns(false)
-
- FileUtils.expects(:rm).with("/former/relative/path")
-
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root"
+ File.stub(:join, "/former/relative/path") do
+ @path.stub(:absolute?, false) do
+ assert_called_with(FileUtils, :rm, ["/former/relative/path"]) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root"
+ end
+ end
+ end
end
def test_when_db_dropped_successfully_outputs_info_to_stdout
- ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root"
+ FileUtils.stub(:rm, nil) do
+ ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root"
- assert_equal "Dropped database '#{@database}'\n", $stdout.string
+ assert_equal "Dropped database '#{@database}'\n", $stdout.string
+ end
end
end
class SqliteDBCharsetTest < ActiveRecord::TestCase
def setup
@database = "db_create.sqlite3"
- @connection = stub :connection
+ @connection = Class.new { def encoding; end }.new
@configuration = {
"adapter" => "sqlite3",
"database" => @database
}
-
- File.stubs(:exist?).returns(false)
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
end
def test_db_retrieves_charset
- @connection.expects(:encoding)
- ActiveRecord::Tasks::DatabaseTasks.charset @configuration, "/rails/root"
+ ActiveRecord::Base.stub(:connection, @connection) do
+ assert_called(@connection, :encoding) do
+ ActiveRecord::Tasks::DatabaseTasks.charset @configuration, "/rails/root"
+ end
+ end
end
end
class SqliteDBCollationTest < ActiveRecord::TestCase
def setup
@database = "db_create.sqlite3"
- @connection = stub :connection
@configuration = {
"adapter" => "sqlite3",
"database" => @database
}
-
- File.stubs(:exist?).returns(false)
- ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).returns(true)
end
def test_db_retrieves_collation
@@ -204,9 +196,9 @@ if current_adapter?(:SQLite3Adapter)
def test_structure_dump_with_ignore_tables
dbfile = @database
filename = "awesome-file.sql"
- ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo"])
-
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root")
+ assert_called(ActiveRecord::SchemaDumper, :ignore_tables, returns: ["foo"]) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root")
+ end
assert File.exist?(dbfile)
assert File.exist?(filename)
assert_match(/bar/, File.read(filename))
@@ -219,14 +211,19 @@ if current_adapter?(:SQLite3Adapter)
def test_structure_dump_execution_fails
dbfile = @database
filename = "awesome-file.sql"
- Kernel.expects(:system).with("sqlite3", "--noop", "db_create.sqlite3", ".schema", out: "awesome-file.sql").returns(nil)
-
- e = assert_raise(RuntimeError) do
- with_structure_dump_flags(["--noop"]) do
- quietly { ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root") }
+ assert_called_with(
+ Kernel,
+ :system,
+ ["sqlite3", "--noop", "db_create.sqlite3", ".schema", out: "awesome-file.sql"],
+ returns: nil
+ ) do
+ e = assert_raise(RuntimeError) do
+ with_structure_dump_flags(["--noop"]) do
+ quietly { ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root") }
+ end
end
+ assert_match("failed to execute:", e.message)
end
- assert_match("failed to execute:", e.message)
ensure
FileUtils.rm_f(filename)
FileUtils.rm_f(dbfile)
diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb
index 024b5bd8a1..5b25432dc0 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require "active_support/test_case"
+require "active_support"
require "active_support/testing/autorun"
require "active_support/testing/method_call_assertions"
require "active_support/testing/stream"
@@ -31,6 +31,7 @@ module ActiveRecord
end
def capture_sql
+ ActiveRecord::Base.connection.materialize_transactions
SQLCounter.clear_log
yield
SQLCounter.log_all.dup
@@ -48,6 +49,7 @@ module ActiveRecord
def assert_queries(num = 1, options = {})
ignore_none = options.fetch(:ignore_none) { num == :any }
+ ActiveRecord::Base.connection.materialize_transactions
SQLCounter.clear_log
x = yield
the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
@@ -77,10 +79,6 @@ module ActiveRecord
model.reset_column_information
model.column_names.include?(column_name.to_s)
end
-
- def frozen_error_class
- Object.const_defined?(:FrozenError) ? FrozenError : RuntimeError
- end
end
class PostgreSQLTestCase < TestCase
diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb
index 086500de38..1abd857216 100644
--- a/activerecord/test/cases/time_precision_test.rb
+++ b/activerecord/test/cases/time_precision_test.rb
@@ -45,6 +45,26 @@ if subsecond_precision_supported?
assert_equal 123456000, foo.finish.nsec
end
+ unless current_adapter?(:Mysql2Adapter)
+ def test_no_time_precision_isnt_truncated_on_assignment
+ @connection.create_table(:foos, force: true)
+ @connection.add_column :foos, :start, :time
+ @connection.add_column :foos, :finish, :time, precision: 6
+
+ time = ::Time.now.change(nsec: 123)
+ foo = Foo.new(start: time, finish: time)
+
+ assert_equal 123, foo.start.nsec
+ assert_equal 0, foo.finish.nsec
+
+ foo.save!
+ foo.reload
+
+ assert_equal 0, foo.start.nsec
+ assert_equal 0, foo.finish.nsec
+ end
+ end
+
def test_passing_precision_to_time_does_not_set_limit
@connection.create_table(:foos, force: true) do |t|
t.time :start, precision: 3
@@ -55,7 +75,7 @@ if subsecond_precision_supported?
end
def test_invalid_time_precision_raises_error
- assert_raises ActiveRecord::ActiveRecordError do
+ assert_raises ArgumentError do
@connection.create_table(:foos, force: true) do |t|
t.time :start, precision: 7
t.time :finish, precision: 7
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index d9f7a81ce4..75ecd6fc40 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -90,8 +90,8 @@ class TimestampTest < ActiveRecord::TestCase
@developer.touch(:created_at)
end
- assert !@developer.created_at_changed?, "created_at should not be changed"
- assert !@developer.changed?, "record should not be changed"
+ assert_not @developer.created_at_changed?, "created_at should not be changed"
+ assert_not @developer.changed?, "record should not be changed"
assert_not_equal previously_created_at, @developer.created_at
assert_not_equal @previously_updated_at, @developer.updated_at
end
diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb
index 925a4609a2..f1a9cf2d05 100644
--- a/activerecord/test/cases/touch_later_test.rb
+++ b/activerecord/test/cases/touch_later_test.rb
@@ -10,7 +10,7 @@ require "models/tree"
class TouchLaterTest < ActiveRecord::TestCase
fixtures :nodes, :trees
- def test_touch_laster_raise_if_non_persisted
+ def test_touch_later_raise_if_non_persisted
invoice = Invoice.new
Invoice.transaction do
assert_not_predicate invoice, :persisted?
@@ -100,7 +100,7 @@ class TouchLaterTest < ActiveRecord::TestCase
def test_touch_later_dont_hit_the_db
invoice = Invoice.create!
- assert_queries(0) do
+ assert_no_queries do
invoice.touch_later
end
end
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index e89ac53732..e88d20a453 100644
--- a/activerecord/test/cases/transaction_callbacks_test.rb
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -38,6 +38,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
before_commit { |record| record.do_before_commit(nil) }
after_commit { |record| record.do_after_commit(nil) }
+ after_save_commit { |record| record.do_after_commit(:save) }
after_create_commit { |record| record.do_after_commit(:create) }
after_update_commit { |record| record.do_after_commit(:update) }
after_destroy_commit { |record| record.do_after_commit(:destroy) }
@@ -110,6 +111,17 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert_equal [:after_commit], @first.history
end
+ def test_only_call_after_commit_on_save_after_transaction_commits_for_saving_record
+ record = TopicWithCallbacks.new(title: "New topic", written_on: Date.today)
+ record.after_commit_block(:save) { |r| r.history << :after_save }
+
+ record.save!
+ assert_equal [:after_save], record.history
+
+ record.update!(title: "Another topic")
+ assert_equal [:after_save, :after_save], record.history
+ end
+
def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record
add_transaction_execution_blocks @first
@@ -139,6 +151,23 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert_equal [], reply.history
end
+ def test_only_call_after_commit_on_destroy_after_transaction_commits_for_destroyed_new_record
+ new_record = TopicWithCallbacks.new(title: "New topic", written_on: Date.today)
+ add_transaction_execution_blocks new_record
+
+ new_record.destroy
+ assert_equal [:commit_on_destroy], new_record.history
+ end
+
+ def test_save_in_after_create_commit_wont_invoke_extra_after_create_commit
+ new_record = TopicWithCallbacks.new(title: "New topic", written_on: Date.today)
+ add_transaction_execution_blocks new_record
+ new_record.after_commit_block(:create) { |r| r.save! }
+
+ new_record.save!
+ assert_equal [:commit_on_create, :commit_on_update], new_record.history
+ end
+
def test_only_call_after_commit_on_create_and_doesnt_leaky
r = ReplyWithCallbacks.new(content: "foo")
r.save_on_after_create = true
@@ -367,6 +396,26 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert_match(/:on conditions for after_commit and after_rollback callbacks have to be one of \[:create, :destroy, :update\]/, e.message)
end
+ def test_after_commit_chain_not_called_on_errors
+ record_1 = TopicWithCallbacks.create!
+ record_2 = TopicWithCallbacks.create!
+ record_3 = TopicWithCallbacks.create!
+ callbacks = []
+ record_1.after_commit_block { raise }
+ record_2.after_commit_block { callbacks << record_2.id }
+ record_3.after_commit_block { callbacks << record_3.id }
+ begin
+ TopicWithCallbacks.transaction do
+ record_1.save!
+ record_2.save!
+ record_3.save!
+ end
+ rescue
+ # From record_1.after_commit
+ end
+ assert_equal [], callbacks
+ end
+
def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_call_callbacks_on_the_parent_object
pet = Pet.first
owner = pet.owner
@@ -554,6 +603,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/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb
index eaafd13360..2932969412 100644
--- a/activerecord/test/cases/transaction_isolation_test.rb
+++ b/activerecord/test/cases/transaction_isolation_test.rb
@@ -11,7 +11,7 @@ unless ActiveRecord::Base.connection.supports_transaction_isolation?
test "setting the isolation level raises an error" do
assert_raises(ActiveRecord::TransactionIsolationError) do
- Tag.transaction(isolation: :serializable) {}
+ Tag.transaction(isolation: :serializable) { Tag.connection.materialize_transactions }
end
end
end
@@ -90,7 +90,7 @@ else
test "setting isolation when joining a transaction raises an error" do
Tag.transaction do
assert_raises(ActiveRecord::TransactionIsolationError) do
- Tag.transaction(isolation: :serializable) {}
+ Tag.transaction(isolation: :serializable) { }
end
end
end
@@ -98,7 +98,7 @@ else
test "setting isolation when starting a nested transaction raises error" do
Tag.transaction do
assert_raises(ActiveRecord::TransactionIsolationError) do
- Tag.transaction(requires_new: true, isolation: :serializable) {}
+ Tag.transaction(requires_new: true, isolation: :serializable) { }
end
end
end
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 1c144a781d..410f07d3ab 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -34,20 +34,21 @@ class TransactionTest < ActiveRecord::TestCase
end
}
- assert @first.reload
assert_not_predicate @first, :frozen?
end
def test_successful
- Topic.transaction do
- @first.approved = true
- @second.approved = false
- @first.save
- @second.save
+ assert_not_called(@first, :committed!) do
+ Topic.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+ @second.save
+ end
end
- assert Topic.find(1).approved?, "First should have been approved"
- assert !Topic.find(2).approved?, "Second should have been unapproved"
+ assert_predicate Topic.find(1), :approved?, "First should have been approved"
+ assert_not_predicate Topic.find(2), :approved?, "Second should have been unapproved"
end
def transaction_with_return
@@ -76,11 +77,13 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- transaction_with_return
+ assert_not_called(@first, :committed!) do
+ transaction_with_return
+ end
assert committed
- assert Topic.find(1).approved?, "First should have been approved"
- assert !Topic.find(2).approved?, "Second should have been unapproved"
+ assert_predicate Topic.find(1), :approved?, "First should have been approved"
+ assert_not_predicate Topic.find(2), :approved?, "Second should have been unapproved"
ensure
Topic.connection.class_eval do
remove_method :commit_db_transaction
@@ -99,9 +102,11 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- Topic.transaction do
- @first.approved = true
- @first.save!
+ assert_not_called(@first, :committed!) do
+ Topic.transaction do
+ @first.approved = true
+ @first.save!
+ end
end
assert_equal 0, num
@@ -113,19 +118,21 @@ class TransactionTest < ActiveRecord::TestCase
end
def test_successful_with_instance_method
- @first.transaction do
- @first.approved = true
- @second.approved = false
- @first.save
- @second.save
+ assert_not_called(@first, :committed!) do
+ @first.transaction do
+ @first.approved = true
+ @second.approved = false
+ @first.save
+ @second.save
+ end
end
- assert Topic.find(1).approved?, "First should have been approved"
- assert !Topic.find(2).approved?, "Second should have been unapproved"
+ assert_predicate Topic.find(1), :approved?, "First should have been approved"
+ assert_not_predicate Topic.find(2), :approved?, "Second should have been unapproved"
end
def test_failing_on_exception
- begin
+ assert_not_called(@first, :rolledback!) do
Topic.transaction do
@first.approved = true
@second.approved = false
@@ -137,11 +144,11 @@ class TransactionTest < ActiveRecord::TestCase
# caught it
end
- assert @first.approved?, "First should still be changed in the objects"
- assert !@second.approved?, "Second should still be changed in the objects"
+ assert_predicate @first, :approved?, "First should still be changed in the objects"
+ assert_not_predicate @second, :approved?, "Second should still be changed in the objects"
- assert !Topic.find(1).approved?, "First shouldn't have been approved"
- assert Topic.find(2).approved?, "Second should still be approved"
+ assert_not_predicate Topic.find(1), :approved?, "First shouldn't have been approved"
+ assert_predicate Topic.find(2), :approved?, "Second should still be approved"
end
def test_raising_exception_in_callback_rollbacks_in_save
@@ -150,8 +157,10 @@ class TransactionTest < ActiveRecord::TestCase
end
@first.approved = true
- e = assert_raises(RuntimeError) { @first.save }
- assert_equal "Make the transaction rollback", e.message
+ assert_not_called(@first, :rolledback!) do
+ e = assert_raises(RuntimeError) { @first.save }
+ assert_equal "Make the transaction rollback", e.message
+ end
assert_not_predicate Topic.find(1), :approved?
end
@@ -159,13 +168,15 @@ class TransactionTest < ActiveRecord::TestCase
def @first.before_save_for_transaction
raise ActiveRecord::Rollback
end
- assert !@first.approved
+ assert_not_predicate @first, :approved?
- Topic.transaction do
- @first.approved = true
- @first.save!
+ assert_not_called(@first, :rolledback!) do
+ Topic.transaction do
+ @first.approved = true
+ @first.save!
+ end
end
- assert !Topic.find(@first.id).approved?, "Should not commit the approved flag"
+ assert_not_predicate Topic.find(@first.id), :approved?, "Should not commit the approved flag"
end
def test_raising_exception_in_nested_transaction_restore_state_in_save
@@ -175,11 +186,13 @@ class TransactionTest < ActiveRecord::TestCase
raise "Make the transaction rollback"
end
- assert_raises(RuntimeError) do
- Topic.transaction { topic.save }
+ assert_not_called(topic, :rolledback!) do
+ assert_raises(RuntimeError) do
+ Topic.transaction { topic.save }
+ end
end
- assert topic.new_record?, "#{topic.inspect} should be new record"
+ assert_predicate topic, :new_record?, "#{topic.inspect} should be new record"
end
def test_transaction_state_is_cleared_when_record_is_persisted
@@ -194,7 +207,7 @@ class TransactionTest < ActiveRecord::TestCase
posts_count = author.posts.size
assert posts_count > 0
status = author.update(name: nil, post_ids: [])
- assert !status
+ assert_not status
assert_equal posts_count, author.posts.reload.size
end
@@ -212,7 +225,7 @@ class TransactionTest < ActiveRecord::TestCase
add_cancelling_before_destroy_with_db_side_effect_to_topic @first
nbooks_before_destroy = Book.count
status = @first.destroy
- assert !status
+ assert_not status
@first.reload
assert_equal nbooks_before_destroy, Book.count
end
@@ -224,7 +237,7 @@ class TransactionTest < ActiveRecord::TestCase
original_author_name = @first.author_name
@first.author_name += "_this_should_not_end_up_in_the_db"
status = @first.save
- assert !status
+ assert_not status
assert_equal original_author_name, @first.reload.author_name
assert_equal nbooks_before_save, Book.count
end
@@ -288,7 +301,19 @@ class TransactionTest < ActiveRecord::TestCase
}
new_topic = topic.create(title: "A new topic")
- assert !new_topic.persisted?, "The topic should not be persisted"
+ assert_not new_topic.persisted?, "The topic should not be persisted"
+ assert_nil new_topic.id, "The topic should not have an ID"
+ end
+
+ def test_callback_rollback_in_create_with_rollback_exception
+ topic = Class.new(Topic) {
+ def after_create_for_transaction
+ raise ActiveRecord::Rollback
+ end
+ }
+
+ new_topic = topic.create(title: "A new topic")
+ assert_not new_topic.persisted?, "The topic should not be persisted"
assert_nil new_topic.id, "The topic should not have an ID"
end
@@ -303,7 +328,7 @@ class TransactionTest < ActiveRecord::TestCase
end
assert Topic.find(1).approved?, "First should have been approved"
- assert !Topic.find(2).approved?, "Second should have been unapproved"
+ assert_not Topic.find(2).approved?, "Second should have been unapproved"
end
def test_nested_transaction_with_new_transaction_applies_parent_state_on_rollback
@@ -387,9 +412,9 @@ class TransactionTest < ActiveRecord::TestCase
end
assert @first.approved?, "First should still be changed in the objects"
- assert !@second.approved?, "Second should still be changed in the objects"
+ assert_not @second.approved?, "Second should still be changed in the objects"
- assert !Topic.find(1).approved?, "First shouldn't have been approved"
+ assert_not Topic.find(1).approved?, "First shouldn't have been approved"
assert Topic.find(2).approved?, "Second should still be approved"
end
@@ -561,10 +586,9 @@ class TransactionTest < ActiveRecord::TestCase
assert_called(Topic.connection, :begin_db_transaction) do
Topic.connection.stub(:commit_db_transaction, -> { raise("OH NOES") }) do
assert_called(Topic.connection, :rollback_db_transaction) do
-
e = assert_raise RuntimeError do
Topic.transaction do
- # do nothing
+ Topic.connection.materialize_transactions
end
end
assert_equal "OH NOES", e.message
@@ -576,12 +600,12 @@ class TransactionTest < ActiveRecord::TestCase
def test_rollback_when_saving_a_frozen_record
topic = Topic.new(title: "test")
topic.freeze
- e = assert_raise(frozen_error_class) { topic.save }
+ e = assert_raise(FrozenError) { topic.save }
# Not good enough, but we can't do much
# about it since there is no specific error
# for frozen objects.
assert_match(/frozen/i, e.message)
- assert !topic.persisted?, "not persisted"
+ assert_not topic.persisted?, "not persisted"
assert_nil topic.id
assert topic.frozen?, "not frozen"
end
@@ -608,9 +632,9 @@ class TransactionTest < ActiveRecord::TestCase
thread.join
assert @first.approved?, "First should still be changed in the objects"
- assert !@second.approved?, "Second should still be changed in the objects"
+ assert_not @second.approved?, "Second should still be changed in the objects"
- assert !Topic.find(1).approved?, "First shouldn't have been approved"
+ assert_not Topic.find(1).approved?, "First shouldn't have been approved"
assert Topic.find(2).approved?, "Second should still be approved"
end
@@ -641,15 +665,15 @@ class TransactionTest < ActiveRecord::TestCase
raise ActiveRecord::Rollback
end
- assert !topic_1.persisted?, "not persisted"
+ assert_not topic_1.persisted?, "not persisted"
assert_nil topic_1.id
- assert !topic_2.persisted?, "not persisted"
+ assert_not topic_2.persisted?, "not persisted"
assert_nil topic_2.id
- assert !topic_3.persisted?, "not persisted"
+ assert_not topic_3.persisted?, "not persisted"
assert_nil topic_3.id
assert @first.persisted?, "persisted"
assert_not_nil @first.id
- assert !@second.destroyed?, "not destroyed"
+ assert_not @second.destroyed?, "not destroyed"
end
def test_restore_frozen_state_after_double_destroy
@@ -667,6 +691,36 @@ class TransactionTest < ActiveRecord::TestCase
assert_not_predicate topic, :frozen?
end
+ def test_restore_new_record_after_double_save
+ topic = Topic.new
+
+ Topic.transaction do
+ topic.save!
+ topic.save!
+ raise ActiveRecord::Rollback
+ end
+
+ assert_nil topic.id
+ assert_predicate topic, :new_record?
+ end
+
+ def test_dont_restore_new_record_in_subsequent_transaction
+ topic = Topic.new
+
+ Topic.transaction do
+ topic.save!
+ topic.save!
+ end
+
+ Topic.transaction do
+ topic.save!
+ raise ActiveRecord::Rollback
+ end
+
+ assert_predicate topic, :persisted?
+ assert_not_predicate topic, :new_record?
+ end
+
def test_restore_id_after_rollback
topic = Topic.new
@@ -843,17 +897,6 @@ class TransactionTest < ActiveRecord::TestCase
assert_predicate transaction.state, :committed?
end
- def test_set_state_method_is_deprecated
- connection = Topic.connection
- transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
-
- transaction.commit
-
- assert_deprecated do
- transaction.state.set_state(:rolledback)
- end
- end
-
def test_mark_transaction_state_as_committed
connection = Topic.connection
transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
@@ -889,7 +932,7 @@ class TransactionTest < ActiveRecord::TestCase
klass = Class.new(ActiveRecord::Base) do
self.table_name = "transaction_without_primary_keys"
- after_commit {} # necessary to trigger the has_transactional_callbacks branch
+ after_commit { } # necessary to trigger the has_transactional_callbacks branch
end
assert_no_difference(-> { klass.count }) do
@@ -902,6 +945,76 @@ class TransactionTest < ActiveRecord::TestCase
connection.drop_table "transaction_without_primary_keys", if_exists: true
end
+ def test_empty_transaction_is_not_materialized
+ assert_no_queries do
+ Topic.transaction { }
+ end
+ end
+
+ def test_unprepared_statement_materializes_transaction
+ assert_sql(/BEGIN/i, /COMMIT/i) do
+ Topic.transaction { Topic.where("1=1").first }
+ end
+ end
+
+ if ActiveRecord::Base.connection.prepared_statements
+ def test_prepared_statement_materializes_transaction
+ Topic.first
+
+ assert_sql(/BEGIN/i, /COMMIT/i) do
+ Topic.transaction { Topic.first }
+ end
+ end
+ end
+
+ def test_savepoint_does_not_materialize_transaction
+ assert_no_queries do
+ Topic.transaction do
+ Topic.transaction(requires_new: true) { }
+ end
+ end
+ end
+
+ def test_raising_does_not_materialize_transaction
+ assert_raise(RuntimeError) do
+ assert_no_queries do
+ Topic.transaction { raise }
+ end
+ end
+ end
+
+ def test_accessing_raw_connection_materializes_transaction
+ assert_sql(/BEGIN/i, /COMMIT/i) do
+ Topic.transaction { Topic.connection.raw_connection }
+ end
+ end
+
+ def test_accessing_raw_connection_disables_lazy_transactions
+ Topic.connection.raw_connection
+
+ assert_sql(/BEGIN/i, /COMMIT/i) do
+ Topic.transaction { }
+ end
+ end
+
+ def test_checking_in_connection_reenables_lazy_transactions
+ connection = Topic.connection_pool.checkout
+ connection.raw_connection
+ Topic.connection_pool.checkin connection
+
+ assert_no_queries do
+ connection.transaction { }
+ end
+ end
+
+ def test_transactions_can_be_manually_materialized
+ assert_sql(/BEGIN/i, /COMMIT/i) do
+ Topic.transaction do
+ Topic.connection.materialize_transactions
+ end
+ end
+ end
+
private
%w(validation save destroy).each do |filter|
diff --git a/activerecord/test/cases/type/time_test.rb b/activerecord/test/cases/type/time_test.rb
new file mode 100644
index 0000000000..1a2c47479f
--- /dev/null
+++ b/activerecord/test/cases/type/time_test.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/topic"
+
+module ActiveRecord
+ module Type
+ class TimeTest < ActiveRecord::TestCase
+ def test_default_year_is_correct
+ expected_time = ::Time.utc(2000, 1, 1, 10, 30, 0)
+ topic = Topic.new(bonus_time: { 4 => 10, 5 => 30 })
+
+ assert_equal expected_time, topic.bonus_time
+
+ topic.save!
+ topic.reload
+
+ assert_equal expected_time, topic.bonus_time
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/type/type_map_test.rb b/activerecord/test/cases/type/type_map_test.rb
index f3699c11a2..1ce515a90c 100644
--- a/activerecord/test/cases/type/type_map_test.rb
+++ b/activerecord/test/cases/type/type_map_test.rb
@@ -32,7 +32,7 @@ module ActiveRecord
end
def test_fuzzy_lookup
- string = String.new
+ string = +""
mapping = TypeMap.new
mapping.register_type(/varchar/i, string)
@@ -41,7 +41,7 @@ module ActiveRecord
end
def test_aliasing_types
- string = String.new
+ string = +""
mapping = TypeMap.new
mapping.register_type(/string/i, string)
@@ -73,7 +73,7 @@ module ActiveRecord
end
def test_register_proc
- string = String.new
+ string = +""
binary = Binary.new
mapping = TypeMap.new
diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb
index f4d8be5897..49746996bc 100644
--- a/activerecord/test/cases/unconnected_test.rb
+++ b/activerecord/test/cases/unconnected_test.rb
@@ -11,6 +11,12 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase
def setup
@underlying = ActiveRecord::Base.connection
@specification = ActiveRecord::Base.remove_connection
+
+ # Clear out connection info from other pids (like a fork parent) too
+ pool_map = ActiveRecord::Base.connection_handler.instance_variable_get(:@owner_to_pool)
+ (pool_map.keys - [Process.pid]).each do |other_pid|
+ pool_map.delete(other_pid)
+ end
end
teardown do
@@ -29,7 +35,15 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase
end
end
+ def test_error_message_when_connection_not_established
+ error = assert_raise(ActiveRecord::ConnectionNotEstablished) do
+ TestRecord.find(1)
+ end
+
+ assert_equal "No connection pool with 'primary' found.", error.message
+ end
+
def test_underlying_adapter_no_longer_active
- assert !@underlying.active?, "Removed adapter should no longer be active"
+ assert_not @underlying.active?, "Removed adapter should no longer be active"
end
end
diff --git a/activerecord/test/cases/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb
index 8235a54d8a..1982734f02 100644
--- a/activerecord/test/cases/validations/absence_validation_test.rb
+++ b/activerecord/test/cases/validations/absence_validation_test.rb
@@ -61,7 +61,7 @@ class AbsenceValidationTest < ActiveRecord::TestCase
def test_validates_absence_of_virtual_attribute_on_model
repair_validations(Interest) do
- Interest.send(:attr_accessor, :token)
+ Interest.attr_accessor(:token)
Interest.validates_absence_of(:token)
interest = Interest.create!(topic: "Thought Leadering")
diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
index 703c24b340..993c201f03 100644
--- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
@@ -4,16 +4,20 @@ require "cases/helper"
require "models/topic"
class I18nGenerateMessageValidationTest < ActiveRecord::TestCase
+ class Backend < I18n::Backend::Simple
+ include I18n::Backend::Fallbacks
+ end
+
def setup
Topic.clear_validators!
@topic = Topic.new
- I18n.backend = I18n::Backend::Simple.new
+ I18n.backend = Backend.new
end
def reset_i18n_load_path
@old_load_path, @old_backend = I18n.load_path.dup, I18n.backend
I18n.load_path.clear
- I18n.backend = I18n::Backend::Simple.new
+ I18n.backend = Backend.new
yield
ensure
I18n.load_path.replace @old_load_path
@@ -83,4 +87,16 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase
assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, value: "title")
end
end
+
+ test "activerecord attributes scope falls back to parent locale before it falls back to the :errors namespace" do
+ reset_i18n_load_path do
+ I18n.backend.store_translations "en", activerecord: { errors: { models: { topic: { attributes: { title: { taken: "custom en message" } } } } } }
+ I18n.backend.store_translations "en-US", errors: { messages: { taken: "generic en-US fallback" } }
+
+ I18n.with_locale "en-US" do
+ assert_equal "custom en message", @topic.errors.generate_message(:title, :taken, value: "title")
+ assert_equal "generic en-US fallback", @topic.errors.generate_message(:heading, :taken, value: "heading")
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb
index 62cd89041a..a7cb718043 100644
--- a/activerecord/test/cases/validations/length_validation_test.rb
+++ b/activerecord/test/cases/validations/length_validation_test.rb
@@ -17,7 +17,7 @@ class LengthValidationTest < ActiveRecord::TestCase
def test_validates_size_of_association
assert_nothing_raised { @owner.validates_size_of :pets, minimum: 1 }
o = @owner.new("name" => "nopets")
- assert !o.save
+ assert_not o.save
assert_predicate o.errors[:pets], :any?
o.pets.build("name" => "apet")
assert_predicate o, :valid?
@@ -26,21 +26,21 @@ class LengthValidationTest < ActiveRecord::TestCase
def test_validates_size_of_association_using_within
assert_nothing_raised { @owner.validates_size_of :pets, within: 1..2 }
o = @owner.new("name" => "nopets")
- assert !o.save
+ assert_not o.save
assert_predicate o.errors[:pets], :any?
o.pets.build("name" => "apet")
assert_predicate o, :valid?
2.times { o.pets.build("name" => "apet") }
- assert !o.save
+ assert_not o.save
assert_predicate o.errors[:pets], :any?
end
def test_validates_size_of_association_utf8
@owner.validates_size_of :pets, minimum: 1
o = @owner.new("name" => "あいうえおかきくけこ")
- assert !o.save
+ assert_not o.save
assert_predicate o.errors[:pets], :any?
o.pets.build("name" => "あいうえおかきくけこ")
assert_predicate o, :valid?
@@ -64,7 +64,7 @@ class LengthValidationTest < ActiveRecord::TestCase
def test_validates_length_of_virtual_attribute_on_model
repair_validations(Pet) do
- Pet.send(:attr_accessor, :nickname)
+ Pet.attr_accessor(:nickname)
Pet.validates_length_of(:name, minimum: 1)
Pet.validates_length_of(:nickname, minimum: 1)
diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb
index 63c3f67da2..4b9cbe9098 100644
--- a/activerecord/test/cases/validations/presence_validation_test.rb
+++ b/activerecord/test/cases/validations/presence_validation_test.rb
@@ -69,7 +69,7 @@ class PresenceValidationTest < ActiveRecord::TestCase
def test_validates_presence_of_virtual_attribute_on_model
repair_validations(Interest) do
- Interest.send(:attr_accessor, :abbreviation)
+ Interest.attr_accessor(:abbreviation)
Interest.validates_presence_of(:topic)
Interest.validates_presence_of(:abbreviation)
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 941aed5402..76163e3093 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -83,8 +83,8 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t.save, "Should still save t as unique"
t2 = Topic.new("title" => "I'm uniqué!")
- assert !t2.valid?, "Shouldn't be valid"
- assert !t2.save, "Shouldn't save t2 as unique"
+ assert_not t2.valid?, "Shouldn't be valid"
+ assert_not t2.save, "Shouldn't save t2 as unique"
assert_equal ["has already been taken"], t2.errors[:title]
t2.title = "Now I am really also unique"
@@ -106,8 +106,8 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t.save, "Should save t as unique"
t2 = Topic.new("title" => nil)
- assert !t2.valid?, "Shouldn't be valid"
- assert !t2.save, "Shouldn't save t2 as unique"
+ assert_not t2.valid?, "Shouldn't be valid"
+ assert_not t2.save, "Shouldn't save t2 as unique"
assert_equal ["has already been taken"], t2.errors[:title]
end
@@ -146,7 +146,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert r1.valid?, "Saving r1"
r2 = t.replies.create "title" => "r2", "content" => "hello world"
- assert !r2.valid?, "Saving r2 first time"
+ assert_not r2.valid?, "Saving r2 first time"
r2.content = "something else"
assert r2.save, "Saving r2 second time"
@@ -172,7 +172,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert r1.valid?, "Saving r1"
r2 = t.replies.create "title" => "r2", "content" => "hello world"
- assert !r2.valid?, "Saving r2 first time"
+ assert_not r2.valid?, "Saving r2 first time"
end
def test_validate_uniqueness_with_polymorphic_object_scope
@@ -193,7 +193,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert r1.valid?, "Saving r1"
r2 = ReplyWithTitleObject.create "title" => "r1", "content" => "hello world"
- assert !r2.valid?, "Saving r2 first time"
+ assert_not r2.valid?, "Saving r2 first time"
end
def test_validate_uniqueness_with_object_arg
@@ -205,7 +205,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert r1.valid?, "Saving r1"
r2 = t.replies.create "title" => "r2", "content" => "hello world"
- assert !r2.valid?, "Saving r2 first time"
+ assert_not r2.valid?, "Saving r2 first time"
end
def test_validate_uniqueness_scoped_to_defining_class
@@ -215,7 +215,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert r1.valid?, "Saving r1"
r2 = t.silly_unique_replies.create "title" => "r2", "content" => "a barrel of fun"
- assert !r2.valid?, "Saving r2"
+ assert_not r2.valid?, "Saving r2"
# Should succeed as validates_uniqueness_of only applies to
# UniqueReply and its subclasses
@@ -232,19 +232,19 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert r1.valid?, "Saving r1"
r2 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy@rubyonrails.com", "title" => "You're crazy!", "content" => "Crazy reply again..."
- assert !r2.valid?, "Saving r2. Double reply by same author."
+ assert_not r2.valid?, "Saving r2. Double reply by same author."
r2.author_email_address = "jeremy_alt_email@rubyonrails.com"
assert r2.save, "Saving r2 the second time."
r3 = t.replies.create "author_name" => "jeremy", "author_email_address" => "jeremy_alt_email@rubyonrails.com", "title" => "You're wrong", "content" => "It's cubic"
- assert !r3.valid?, "Saving r3"
+ assert_not r3.valid?, "Saving r3"
r3.author_name = "jj"
assert r3.save, "Saving r3 the second time."
r3.author_name = "jeremy"
- assert !r3.save, "Saving r3 the third time."
+ assert_not r3.save, "Saving r3 the third time."
end
def test_validate_case_insensitive_uniqueness
@@ -257,15 +257,15 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t.save, "Should still save t as unique"
t2 = Topic.new("title" => "I'm UNIQUE!", :parent_id => 1)
- assert !t2.valid?, "Shouldn't be valid"
- assert !t2.save, "Shouldn't save t2 as unique"
+ assert_not t2.valid?, "Shouldn't be valid"
+ assert_not t2.save, "Shouldn't save t2 as unique"
assert_predicate t2.errors[:title], :any?
assert_predicate t2.errors[:parent_id], :any?
assert_equal ["has already been taken"], t2.errors[:title]
t2.title = "I'm truly UNIQUE!"
- assert !t2.valid?, "Shouldn't be valid"
- assert !t2.save, "Shouldn't save t2 as unique"
+ assert_not t2.valid?, "Shouldn't be valid"
+ assert_not t2.save, "Shouldn't save t2 as unique"
assert_empty t2.errors[:title]
assert_predicate t2.errors[:parent_id], :any?
@@ -283,8 +283,8 @@ class UniquenessValidationTest < ActiveRecord::TestCase
# If database hasn't UTF-8 character set, this test fails
if Topic.all.merge!(select: "LOWER(title) AS title").find(t_utf8.id).title == "я тоже уникальный!"
t2_utf8 = Topic.new("title" => "я тоже УНИКАЛЬНЫЙ!")
- assert !t2_utf8.valid?, "Shouldn't be valid"
- assert !t2_utf8.save, "Shouldn't save t2_utf8 as unique"
+ assert_not t2_utf8.valid?, "Shouldn't be valid"
+ assert_not t2_utf8.save, "Shouldn't save t2_utf8 as unique"
end
end
@@ -314,6 +314,51 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert t3.save, "Should save t3 as unique"
end
+ if current_adapter?(:Mysql2Adapter)
+ def test_deprecate_validate_uniqueness_mismatched_collation
+ Topic.validates_uniqueness_of(:author_email_address)
+
+ topic1 = Topic.new(author_email_address: "david@loudthinking.com")
+ topic2 = Topic.new(author_email_address: "David@loudthinking.com")
+
+ assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count
+
+ assert_deprecated do
+ assert_not topic1.valid?
+ assert_not topic1.save
+ assert topic2.valid?
+ assert topic2.save
+ end
+
+ assert_equal 2, Topic.where(author_email_address: "david@loudthinking.com").count
+ assert_equal 2, Topic.where(author_email_address: "David@loudthinking.com").count
+ end
+ end
+
+ def test_validate_case_sensitive_uniqueness_by_default
+ Topic.validates_uniqueness_of(:author_email_address)
+
+ topic1 = Topic.new(author_email_address: "david@loudthinking.com")
+ topic2 = Topic.new(author_email_address: "David@loudthinking.com")
+
+ assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count
+
+ ActiveSupport::Deprecation.silence do
+ assert_not topic1.valid?
+ assert_not topic1.save
+ assert topic2.valid?
+ assert topic2.save
+ end
+
+ if current_adapter?(:Mysql2Adapter)
+ assert_equal 2, Topic.where(author_email_address: "david@loudthinking.com").count
+ assert_equal 2, Topic.where(author_email_address: "David@loudthinking.com").count
+ else
+ assert_equal 1, Topic.where(author_email_address: "david@loudthinking.com").count
+ assert_equal 1, Topic.where(author_email_address: "David@loudthinking.com").count
+ end
+ end
+
def test_validate_case_sensitive_uniqueness
Topic.validates_uniqueness_of(:title, case_sensitive: true, allow_nil: true)
@@ -349,7 +394,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
def test_validate_uniqueness_with_non_standard_table_names
i1 = WarehouseThing.create(value: 1000)
- assert !i1.valid?, "i1 should not be valid"
+ assert_not i1.valid?, "i1 should not be valid"
assert i1.errors[:value].any?, "Should not be empty"
end
@@ -417,12 +462,12 @@ class UniquenessValidationTest < ActiveRecord::TestCase
# Should use validation from base class (which is abstract)
w2 = IneptWizard.new(name: "Rincewind", city: "Quirm")
- assert !w2.valid?, "w2 shouldn't be valid"
+ assert_not w2.valid?, "w2 shouldn't be valid"
assert w2.errors[:name].any?, "Should have errors for name"
assert_equal ["has already been taken"], w2.errors[:name], "Should have uniqueness message for name"
w3 = Conjurer.new(name: "Rincewind", city: "Quirm")
- assert !w3.valid?, "w3 shouldn't be valid"
+ assert_not w3.valid?, "w3 shouldn't be valid"
assert w3.errors[:name].any?, "Should have errors for name"
assert_equal ["has already been taken"], w3.errors[:name], "Should have uniqueness message for name"
@@ -430,12 +475,12 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert w4.valid?, "Saving w4"
w5 = Thaumaturgist.new(name: "The Amazing Bonko", city: "Lancre")
- assert !w5.valid?, "w5 shouldn't be valid"
+ assert_not w5.valid?, "w5 shouldn't be valid"
assert w5.errors[:name].any?, "Should have errors for name"
assert_equal ["has already been taken"], w5.errors[:name], "Should have uniqueness message for name"
w6 = Thaumaturgist.new(name: "Mustrum Ridcully", city: "Quirm")
- assert !w6.valid?, "w6 shouldn't be valid"
+ assert_not w6.valid?, "w6 shouldn't be valid"
assert w6.errors[:city].any?, "Should have errors for city"
assert_equal ["has already been taken"], w6.errors[:city], "Should have uniqueness message for city"
end
@@ -446,7 +491,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
Topic.create("title" => "I'm an unapproved topic", "approved" => false)
t3 = Topic.new("title" => "I'm a topic", "approved" => true)
- assert !t3.valid?, "t3 shouldn't be valid"
+ assert_not t3.valid?, "t3 shouldn't be valid"
t4 = Topic.new("title" => "I'm an unapproved topic", "approved" => false)
assert t4.valid?, "t4 should be valid"
@@ -510,7 +555,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
abc.save!
end
assert_match(/\AUnknown primary key for table dashboards in model/, e.message)
- assert_match(/Can not validate uniqueness for persisted record without primary key.\z/, e.message)
+ assert_match(/Cannot validate uniqueness for persisted record without primary key.\z/, e.message)
end
def test_validate_uniqueness_ignores_itself_when_primary_key_changed
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index 6c83bbd15c..9a70934b7e 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -3,11 +3,11 @@
require "cases/helper"
require "models/topic"
require "models/reply"
-require "models/person"
require "models/developer"
require "models/computer"
require "models/parrot"
require "models/company"
+require "models/price_estimate"
class ValidationsTest < ActiveRecord::TestCase
fixtures :topics, :developers
@@ -39,7 +39,7 @@ class ValidationsTest < ActiveRecord::TestCase
def test_valid_using_special_context
r = WrongReply.new(title: "Valid title")
- assert !r.valid?(:special_case)
+ assert_not r.valid?(:special_case)
assert_equal "Invalid", r.errors[:author_name].join
r.author_name = "secret"
@@ -125,7 +125,7 @@ class ValidationsTest < ActiveRecord::TestCase
def test_save_without_validation
reply = WrongReply.new
- assert !reply.save
+ assert_not reply.save
assert reply.save(validate: false)
end
@@ -144,6 +144,13 @@ class ValidationsTest < ActiveRecord::TestCase
assert_equal "100,000", d.salary_before_type_cast
end
+ def test_validates_acceptance_of_with_undefined_attribute_methods
+ Topic.validates_acceptance_of(:approved)
+ topic = Topic.new(approved: true)
+ Topic.undefine_attribute_methods
+ assert topic.approved
+ end
+
def test_validates_acceptance_of_as_database_column
Topic.validates_acceptance_of(:approved)
topic = Topic.create("approved" => true)
@@ -183,6 +190,22 @@ class ValidationsTest < ActiveRecord::TestCase
assert_not_predicate klass.new(wibble: BigDecimal("97.179")), :valid?
end
+ def test_numericality_validator_wont_be_affected_by_custom_getter
+ price_estimate = PriceEstimate.new(price: 50)
+
+ assert_equal "$50.00", price_estimate.price
+ assert_equal 50, price_estimate.price_before_type_cast
+ assert_equal 50, price_estimate.read_attribute(:price)
+
+ assert_predicate price_estimate, :price_came_from_user?
+ assert_predicate price_estimate, :valid?
+
+ price_estimate.save!
+
+ assert_not_predicate price_estimate, :price_came_from_user?
+ assert_predicate price_estimate, :valid?
+ end
+
def test_acceptance_validator_doesnt_require_db_connection
klass = Class.new(ActiveRecord::Base) do
self.table_name = "posts"
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