aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord
diff options
context:
space:
mode:
Diffstat (limited to 'activerecord')
-rw-r--r--activerecord/.gitignore3
-rw-r--r--activerecord/CHANGELOG.md242
-rw-r--r--activerecord/MIT-LICENSE4
-rw-r--r--activerecord/README.rdoc4
-rw-r--r--activerecord/Rakefile6
-rw-r--r--activerecord/activerecord.gemspec4
-rwxr-xr-xactiverecord/bin/test7
-rw-r--r--activerecord/examples/.gitignore1
-rw-r--r--activerecord/lib/active_record.rb15
-rw-r--r--activerecord/lib/active_record/aggregations.rb9
-rw-r--r--activerecord/lib/active_record/association_relation.rb4
-rw-r--r--activerecord/lib/active_record/associations.rb79
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb45
-rw-r--r--activerecord/lib/active_record/associations/association.rb10
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb21
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb28
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb7
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb4
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb10
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb2
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb28
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb16
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb21
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb1
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb17
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb36
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb19
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_base.rb2
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb58
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb51
-rw-r--r--activerecord/lib/active_record/associations/preloader/belongs_to.rb10
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb16
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many.rb10
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb21
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one.rb10
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_one_through.rb11
-rw-r--r--activerecord/lib/active_record/associations/preloader/singular_association.rb15
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb108
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb31
-rw-r--r--activerecord/lib/active_record/attribute.rb242
-rw-r--r--activerecord/lib/active_record/attribute/user_provided_default.rb32
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb6
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb92
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb114
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb10
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb9
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb7
-rw-r--r--activerecord/lib/active_record/attribute_mutation_tracker.rb111
-rw-r--r--activerecord/lib/active_record/attribute_set.rb113
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb126
-rw-r--r--activerecord/lib/active_record/attribute_set/yaml_encoder.rb43
-rw-r--r--activerecord/lib/active_record/attributes.rb8
-rw-r--r--activerecord/lib/active_record/autosave_association.rb3
-rw-r--r--activerecord/lib/active_record/base.rb18
-rw-r--r--activerecord/lib/active_record/callbacks.rb28
-rw-r--r--activerecord/lib/active_record/collection_cache_key.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb122
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb80
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb45
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb134
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb77
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb183
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb3
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb7
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb35
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/column.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb43
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb5
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb23
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb42
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb33
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb12
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb250
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb81
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb4
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb19
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb24
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb106
-rw-r--r--activerecord/lib/active_record/connection_handling.rb6
-rw-r--r--activerecord/lib/active_record/core.rb44
-rw-r--r--activerecord/lib/active_record/counter_cache.rb2
-rw-r--r--activerecord/lib/active_record/database_configurations.rb63
-rw-r--r--activerecord/lib/active_record/enum.rb7
-rw-r--r--activerecord/lib/active_record/errors.rb42
-rw-r--r--activerecord/lib/active_record/explain.rb2
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb2
-rw-r--r--activerecord/lib/active_record/fixtures.rb74
-rw-r--r--activerecord/lib/active_record/gem_version.rb4
-rw-r--r--activerecord/lib/active_record/inheritance.rb65
-rw-r--r--activerecord/lib/active_record/internal_metadata.rb6
-rw-r--r--activerecord/lib/active_record/legacy_yaml_adapter.rb2
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb68
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb11
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb41
-rw-r--r--activerecord/lib/active_record/migration.rb295
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb8
-rw-r--r--activerecord/lib/active_record/migration/compatibility.rb92
-rw-r--r--activerecord/lib/active_record/model_schema.rb87
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb12
-rw-r--r--activerecord/lib/active_record/persistence.rb157
-rw-r--r--activerecord/lib/active_record/query_cache.rb24
-rw-r--r--activerecord/lib/active_record/querying.rb4
-rw-r--r--activerecord/lib/active_record/railtie.rb35
-rw-r--r--activerecord/lib/active_record/railties/collection_cache_association_loading.rb34
-rw-r--r--activerecord/lib/active_record/railties/controller_runtime.rb67
-rw-r--r--activerecord/lib/active_record/railties/databases.rake151
-rw-r--r--activerecord/lib/active_record/railties/jdbcmysql_error.rb18
-rw-r--r--activerecord/lib/active_record/reflection.rb85
-rw-r--r--activerecord/lib/active_record/relation.rb249
-rw-r--r--activerecord/lib/active_record/relation/batches.rb28
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb53
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb7
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb107
-rw-r--r--activerecord/lib/active_record/relation/merger.rb46
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb31
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb3
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb7
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/base_handler.rb3
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb3
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb17
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/range_handler.rb10
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb4
-rw-r--r--activerecord/lib/active_record/relation/query_attribute.rb21
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb109
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb5
-rw-r--r--activerecord/lib/active_record/relation/where_clause.rb18
-rw-r--r--activerecord/lib/active_record/relation/where_clause_factory.rb6
-rw-r--r--activerecord/lib/active_record/sanitization.rb254
-rw-r--r--activerecord/lib/active_record/schema.rb2
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb43
-rw-r--r--activerecord/lib/active_record/schema_migration.rb4
-rw-r--r--activerecord/lib/active_record/scoping.rb10
-rw-r--r--activerecord/lib/active_record/scoping/default.rb4
-rw-r--r--activerecord/lib/active_record/scoping/named.rb16
-rw-r--r--activerecord/lib/active_record/statement_cache.rb3
-rw-r--r--activerecord/lib/active_record/store.rb31
-rw-r--r--activerecord/lib/active_record/table_metadata.rb16
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb86
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb47
-rw-r--r--activerecord/lib/active_record/test_databases.rb38
-rw-r--r--activerecord/lib/active_record/timestamp.rb22
-rw-r--r--activerecord/lib/active_record/transactions.rb2
-rw-r--r--activerecord/lib/active_record/translation.rb2
-rw-r--r--activerecord/lib/active_record/type.rb24
-rw-r--r--activerecord/lib/active_record/type/adapter_specific_registry.rb9
-rw-r--r--activerecord/lib/active_record/type_caster.rb4
-rw-r--r--activerecord/lib/active_record/type_caster/connection.rb7
-rw-r--r--activerecord/lib/active_record/type_caster/map.rb7
-rw-r--r--activerecord/lib/active_record/validations.rb10
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb6
-rw-r--r--activerecord/lib/arel.rb40
-rw-r--r--activerecord/lib/arel/alias_predication.rb9
-rw-r--r--activerecord/lib/arel/attributes.rb22
-rw-r--r--activerecord/lib/arel/attributes/attribute.rb37
-rw-r--r--activerecord/lib/arel/collectors/bind.rb24
-rw-r--r--activerecord/lib/arel/collectors/composite.rb32
-rw-r--r--activerecord/lib/arel/collectors/plain_string.rb20
-rw-r--r--activerecord/lib/arel/collectors/sql_string.rb24
-rw-r--r--activerecord/lib/arel/collectors/substitute_binds.rb29
-rw-r--r--activerecord/lib/arel/compatibility/wheres.rb35
-rw-r--r--activerecord/lib/arel/crud.rb42
-rw-r--r--activerecord/lib/arel/delete_manager.rb25
-rw-r--r--activerecord/lib/arel/errors.rb9
-rw-r--r--activerecord/lib/arel/expressions.rb29
-rw-r--r--activerecord/lib/arel/factory_methods.rb45
-rw-r--r--activerecord/lib/arel/insert_manager.rb49
-rw-r--r--activerecord/lib/arel/math.rb45
-rw-r--r--activerecord/lib/arel/nodes.rb67
-rw-r--r--activerecord/lib/arel/nodes/and.rb32
-rw-r--r--activerecord/lib/arel/nodes/ascending.rb23
-rw-r--r--activerecord/lib/arel/nodes/binary.rb52
-rw-r--r--activerecord/lib/arel/nodes/bind_param.rb28
-rw-r--r--activerecord/lib/arel/nodes/case.rb55
-rw-r--r--activerecord/lib/arel/nodes/casted.rb46
-rw-r--r--activerecord/lib/arel/nodes/count.rb12
-rw-r--r--activerecord/lib/arel/nodes/delete_statement.rb38
-rw-r--r--activerecord/lib/arel/nodes/descending.rb23
-rw-r--r--activerecord/lib/arel/nodes/equality.rb11
-rw-r--r--activerecord/lib/arel/nodes/extract.rb24
-rw-r--r--activerecord/lib/arel/nodes/false.rb16
-rw-r--r--activerecord/lib/arel/nodes/full_outer_join.rb8
-rw-r--r--activerecord/lib/arel/nodes/function.rb44
-rw-r--r--activerecord/lib/arel/nodes/grouping.rb8
-rw-r--r--activerecord/lib/arel/nodes/in.rb8
-rw-r--r--activerecord/lib/arel/nodes/infix_operation.rb80
-rw-r--r--activerecord/lib/arel/nodes/inner_join.rb8
-rw-r--r--activerecord/lib/arel/nodes/insert_statement.rb37
-rw-r--r--activerecord/lib/arel/nodes/join_source.rb20
-rw-r--r--activerecord/lib/arel/nodes/matches.rb18
-rw-r--r--activerecord/lib/arel/nodes/named_function.rb23
-rw-r--r--activerecord/lib/arel/nodes/node.rb60
-rw-r--r--activerecord/lib/arel/nodes/node_expression.rb13
-rw-r--r--activerecord/lib/arel/nodes/outer_join.rb8
-rw-r--r--activerecord/lib/arel/nodes/over.rb15
-rw-r--r--activerecord/lib/arel/nodes/regexp.rb16
-rw-r--r--activerecord/lib/arel/nodes/right_outer_join.rb8
-rw-r--r--activerecord/lib/arel/nodes/select_core.rb65
-rw-r--r--activerecord/lib/arel/nodes/select_statement.rb41
-rw-r--r--activerecord/lib/arel/nodes/sql_literal.rb16
-rw-r--r--activerecord/lib/arel/nodes/string_join.rb11
-rw-r--r--activerecord/lib/arel/nodes/table_alias.rb27
-rw-r--r--activerecord/lib/arel/nodes/terminal.rb16
-rw-r--r--activerecord/lib/arel/nodes/true.rb16
-rw-r--r--activerecord/lib/arel/nodes/unary.rb45
-rw-r--r--activerecord/lib/arel/nodes/unary_operation.rb20
-rw-r--r--activerecord/lib/arel/nodes/unqualified_column.rb22
-rw-r--r--activerecord/lib/arel/nodes/update_statement.rb40
-rw-r--r--activerecord/lib/arel/nodes/values.rb16
-rw-r--r--activerecord/lib/arel/nodes/values_list.rb24
-rw-r--r--activerecord/lib/arel/nodes/window.rb126
-rw-r--r--activerecord/lib/arel/nodes/with.rb11
-rw-r--r--activerecord/lib/arel/order_predications.rb13
-rw-r--r--activerecord/lib/arel/predications.rb241
-rw-r--r--activerecord/lib/arel/select_manager.rb273
-rw-r--r--activerecord/lib/arel/table.rb111
-rw-r--r--activerecord/lib/arel/tree_manager.rb38
-rw-r--r--activerecord/lib/arel/update_manager.rb59
-rw-r--r--activerecord/lib/arel/visitors.rb20
-rw-r--r--activerecord/lib/arel/visitors/depth_first.rb200
-rw-r--r--activerecord/lib/arel/visitors/dot.rb292
-rw-r--r--activerecord/lib/arel/visitors/ibm_db.rb15
-rw-r--r--activerecord/lib/arel/visitors/informix.rb55
-rw-r--r--activerecord/lib/arel/visitors/mssql.rb125
-rw-r--r--activerecord/lib/arel/visitors/mysql.rb87
-rw-r--r--activerecord/lib/arel/visitors/oracle.rb153
-rw-r--r--activerecord/lib/arel/visitors/oracle12.rb61
-rw-r--r--activerecord/lib/arel/visitors/postgresql.rb104
-rw-r--r--activerecord/lib/arel/visitors/sqlite.rb27
-rw-r--r--activerecord/lib/arel/visitors/to_sql.rb847
-rw-r--r--activerecord/lib/arel/visitors/visitor.rb42
-rw-r--r--activerecord/lib/arel/visitors/where_sql.rb23
-rw-r--r--activerecord/lib/arel/window_predications.rb9
-rw-r--r--activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt (renamed from activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb)0
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb6
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt (renamed from activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb)0
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt (renamed from activerecord/lib/rails/generators/active_record/migration/templates/migration.rb)0
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt (renamed from activerecord/lib/rails/generators/active_record/model/templates/model.rb)0
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/module.rb.tt (renamed from activerecord/lib/rails/generators/active_record/model/templates/module.rb)0
-rw-r--r--activerecord/test/.gitignore1
-rw-r--r--activerecord/test/cases/adapter_test.rb14
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb14
-rw-r--r--activerecord/test/cases/adapters/mysql2/auto_increment_test.rb34
-rw-r--r--activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb28
-rw-r--r--activerecord/test/cases/adapters/mysql2/enum_test.rb4
-rw-r--r--activerecord/test/cases/adapters/mysql2/explain_test.rb22
-rw-r--r--activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb10
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/table_options_test.rb75
-rw-r--r--activerecord/test/cases/adapters/mysql2/transaction_test.rb90
-rw-r--r--activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb9
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/bit_string_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/change_schema_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/citext_test.rb110
-rw-r--r--activerecord/test/cases/adapters/postgresql/composite_test.rb8
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/date_test.rb42
-rw-r--r--activerecord/test/cases/adapters/postgresql/domain_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/enum_test.rb6
-rw-r--r--activerecord/test/cases/adapters/postgresql/explain_test.rb16
-rw-r--r--activerecord/test/cases/adapters/postgresql/extension_migration_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/foreign_table_test.rb109
-rw-r--r--activerecord/test/cases/adapters/postgresql/full_text_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/geometric_test.rb12
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb590
-rw-r--r--activerecord/test/cases/adapters/postgresql/infinity_test.rb39
-rw-r--r--activerecord/test/cases/adapters/postgresql/ltree_test.rb4
-rw-r--r--activerecord/test/cases/adapters/postgresql/money_test.rb10
-rw-r--r--activerecord/test/cases/adapters/postgresql/network_test.rb12
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb30
-rw-r--r--activerecord/test/cases/adapters/postgresql/range_test.rb77
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb64
-rw-r--r--activerecord/test/cases/adapters/postgresql/serial_test.rb42
-rw-r--r--activerecord/test/cases/adapters/postgresql/statement_pool_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/transaction_test.rb90
-rw-r--r--activerecord/test/cases/adapters/postgresql/type_lookup_test.rb2
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb198
-rw-r--r--activerecord/test/cases/adapters/sqlite3/explain_test.rb22
-rw-r--r--activerecord/test/cases/adapters/sqlite3/json_test.rb29
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb9
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb126
-rw-r--r--activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb2
-rw-r--r--activerecord/test/cases/aggregations_test.rb2
-rw-r--r--activerecord/test/cases/ar_schema_test.rb16
-rw-r--r--activerecord/test/cases/arel/attributes/attribute_test.rb1015
-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.rb47
-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.rb52
-rw-r--r--activerecord/test/cases/arel/factory_methods_test.rb46
-rw-r--r--activerecord/test/cases/arel/helper.rb44
-rw-r--r--activerecord/test/cases/arel/insert_manager_test.rb244
-rw-r--r--activerecord/test/cases/arel/nodes/and_test.rb21
-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.rb27
-rw-r--r--activerecord/test/cases/arel/nodes/bind_param_test.rb22
-rw-r--r--activerecord/test/cases/arel/nodes/case_test.rb84
-rw-r--r--activerecord/test/cases/arel/nodes/casted_test.rb18
-rw-r--r--activerecord/test/cases/arel/nodes/count_test.rb44
-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.rb71
-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.rb1236
-rw-r--r--activerecord/test/cases/arel/support/fake_record.rb129
-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.rb271
-rw-r--r--activerecord/test/cases/arel/visitors/dispatch_contamination_test.rb72
-rw-r--r--activerecord/test/cases/arel/visitors/dot_test.rb84
-rw-r--r--activerecord/test/cases/arel/visitors/ibm_db_test.rb34
-rw-r--r--activerecord/test/cases/arel/visitors/informix_test.rb59
-rw-r--r--activerecord/test/cases/arel/visitors/mssql_test.rb99
-rw-r--r--activerecord/test/cases/arel/visitors/mysql_test.rb80
-rw-r--r--activerecord/test/cases/arel/visitors/oracle12_test.rb61
-rw-r--r--activerecord/test/cases/arel/visitors/oracle_test.rb197
-rw-r--r--activerecord/test/cases/arel/visitors/postgres_test.rb281
-rw-r--r--activerecord/test/cases/arel/visitors/sqlite_test.rb32
-rw-r--r--activerecord/test/cases/arel/visitors/to_sql_test.rb654
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb79
-rw-r--r--activerecord/test/cases/associations/callbacks_test.rb16
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb4
-rw-r--r--activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb58
-rw-r--r--activerecord/test/cases/associations/eager_test.rb75
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb105
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb292
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb129
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb61
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb48
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb30
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb55
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb40
-rw-r--r--activerecord/test/cases/associations/left_outer_join_association_test.rb11
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb67
-rw-r--r--activerecord/test/cases/associations_test.rb32
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb2
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb84
-rw-r--r--activerecord/test/cases/attribute_set_test.rb255
-rw-r--r--activerecord/test/cases/attribute_test.rb255
-rw-r--r--activerecord/test/cases/attributes_test.rb8
-rw-r--r--activerecord/test/cases/autosave_association_test.rb370
-rw-r--r--activerecord/test/cases/base_test.rb160
-rw-r--r--activerecord/test/cases/batches_test.rb48
-rw-r--r--activerecord/test/cases/cache_key_test.rb4
-rw-r--r--activerecord/test/cases/calculations_test.rb56
-rw-r--r--activerecord/test/cases/callbacks_test.rb36
-rw-r--r--activerecord/test/cases/clone_test.rb2
-rw-r--r--activerecord/test/cases/collection_cache_key_test.rb36
-rw-r--r--activerecord/test/cases/comment_test.rb22
-rw-r--r--activerecord/test/cases/connection_adapters/adapter_leasing_test.rb4
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handler_test.rb142
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb8
-rw-r--r--activerecord/test/cases/connection_adapters/type_lookup_test.rb6
-rw-r--r--activerecord/test/cases/connection_management_test.rb14
-rw-r--r--activerecord/test/cases/connection_pool_test.rb86
-rw-r--r--activerecord/test/cases/connection_specification/resolver_test.rb2
-rw-r--r--activerecord/test/cases/core_test.rb81
-rw-r--r--activerecord/test/cases/date_time_precision_test.rb18
-rw-r--r--activerecord/test/cases/defaults_test.rb2
-rw-r--r--activerecord/test/cases/dirty_test.rb221
-rw-r--r--activerecord/test/cases/dup_test.rb32
-rw-r--r--activerecord/test/cases/enum_test.rb158
-rw-r--r--activerecord/test/cases/explain_subscriber_test.rb10
-rw-r--r--activerecord/test/cases/explain_test.rb3
-rw-r--r--activerecord/test/cases/finder_respond_to_test.rb12
-rw-r--r--activerecord/test/cases/finder_test.rb144
-rw-r--r--activerecord/test/cases/fixtures_test.rb171
-rw-r--r--activerecord/test/cases/habtm_destroy_order_test.rb6
-rw-r--r--activerecord/test/cases/helper.rb2
-rw-r--r--activerecord/test/cases/inheritance_test.rb76
-rw-r--r--activerecord/test/cases/instrumentation_test.rb72
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb45
-rw-r--r--activerecord/test/cases/json_attribute_test.rb6
-rw-r--r--activerecord/test/cases/json_serialization_test.rb2
-rw-r--r--activerecord/test/cases/json_shared_test_cases.rb55
-rw-r--r--activerecord/test/cases/locking_test.rb141
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb21
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb23
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb6
-rw-r--r--activerecord/test/cases/migration/columns_test.rb14
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb9
-rw-r--r--activerecord/test/cases/migration/compatibility_test.rb159
-rw-r--r--activerecord/test/cases/migration/create_join_table_test.rb14
-rw-r--r--activerecord/test/cases/migration/foreign_key_test.rb79
-rw-r--r--activerecord/test/cases/migration/index_test.rb14
-rw-r--r--activerecord/test/cases/migration/pending_migrations_test.rb50
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb6
-rw-r--r--activerecord/test/cases/migration_test.rb174
-rw-r--r--activerecord/test/cases/migrator_test.rb152
-rw-r--r--activerecord/test/cases/multiparameter_attributes_test.rb8
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb40
-rw-r--r--activerecord/test/cases/nested_attributes_with_callbacks_test.rb10
-rw-r--r--activerecord/test/cases/numeric_data_test.rb14
-rw-r--r--activerecord/test/cases/persistence_test.rb324
-rw-r--r--activerecord/test/cases/primary_keys_test.rb24
-rw-r--r--activerecord/test/cases/query_cache_test.rb53
-rw-r--r--activerecord/test/cases/quoting_test.rb78
-rw-r--r--activerecord/test/cases/readonly_test.rb56
-rw-r--r--activerecord/test/cases/reaper_test.rb16
-rw-r--r--activerecord/test/cases/reflection_test.rb151
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb28
-rw-r--r--activerecord/test/cases/relation/merging_test.rb16
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb11
-rw-r--r--activerecord/test/cases/relation/or_test.rb7
-rw-r--r--activerecord/test/cases/relation/select_test.rb15
-rw-r--r--activerecord/test/cases/relation/where_clause_test.rb4
-rw-r--r--activerecord/test/cases/relation/where_test.rb2
-rw-r--r--activerecord/test/cases/relation_test.rb91
-rw-r--r--activerecord/test/cases/relations_test.rb384
-rw-r--r--activerecord/test/cases/reserved_word_test.rb11
-rw-r--r--activerecord/test/cases/sanitize_test.rb79
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb72
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb48
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb92
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb36
-rw-r--r--activerecord/test/cases/serialization_test.rb4
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb4
-rw-r--r--activerecord/test/cases/statement_cache_test.rb5
-rw-r--r--activerecord/test/cases/store_test.rb28
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb554
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb150
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb106
-rw-r--r--activerecord/test/cases/tasks/sqlite_rake_test.rb23
-rw-r--r--activerecord/test/cases/test_case.rb8
-rw-r--r--activerecord/test/cases/time_precision_test.rb18
-rw-r--r--activerecord/test/cases/timestamp_test.rb34
-rw-r--r--activerecord/test/cases/touch_later_test.rb4
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb30
-rw-r--r--activerecord/test/cases/transaction_isolation_test.rb4
-rw-r--r--activerecord/test/cases/transactions_test.rb108
-rw-r--r--activerecord/test/cases/type/string_test.rb6
-rw-r--r--activerecord/test/cases/unsafe_raw_sql_test.rb319
-rw-r--r--activerecord/test/cases/validations/absence_validation_test.rb10
-rw-r--r--activerecord/test/cases/validations/association_validation_test.rb24
-rw-r--r--activerecord/test/cases/validations/length_validation_test.rb36
-rw-r--r--activerecord/test/cases/validations/presence_validation_test.rb18
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb42
-rw-r--r--activerecord/test/cases/validations_test.rb18
-rw-r--r--activerecord/test/cases/yaml_serialization_test.rb4
-rw-r--r--activerecord/test/fixtures/.gitignore1
-rw-r--r--activerecord/test/fixtures/customers.yml11
-rw-r--r--activerecord/test/fixtures/minimalistics.yml3
-rw-r--r--activerecord/test/fixtures/other_posts.yml1
-rw-r--r--activerecord/test/fixtures/posts.yml8
-rw-r--r--activerecord/test/fixtures/reserved_words/values.yml4
-rw-r--r--activerecord/test/fixtures/sponsors.yml3
-rw-r--r--activerecord/test/fixtures/teapots.yml3
-rw-r--r--activerecord/test/migrations/decimal/1_give_me_big_numbers.rb2
-rw-r--r--activerecord/test/models/admin/user.rb3
-rw-r--r--activerecord/test/models/author.rb12
-rw-r--r--activerecord/test/models/comment.rb8
-rw-r--r--activerecord/test/models/company.rb2
-rw-r--r--activerecord/test/models/contract.rb2
-rw-r--r--activerecord/test/models/customer.rb2
-rw-r--r--activerecord/test/models/developer.rb11
-rw-r--r--activerecord/test/models/drink_designer.rb3
-rw-r--r--activerecord/test/models/face.rb5
-rw-r--r--activerecord/test/models/frog.rb8
-rw-r--r--activerecord/test/models/man.rb3
-rw-r--r--activerecord/test/models/member_detail.rb1
-rw-r--r--activerecord/test/models/mocktail_designer.rb4
-rw-r--r--activerecord/test/models/post.rb18
-rw-r--r--activerecord/test/models/sponsor.rb1
-rw-r--r--activerecord/test/models/tag.rb3
-rw-r--r--activerecord/test/models/tagging.rb5
-rw-r--r--activerecord/test/models/topic.rb15
-rw-r--r--activerecord/test/models/wheel.rb2
-rw-r--r--activerecord/test/schema/schema.rb23
513 files changed, 21120 insertions, 6701 deletions
diff --git a/activerecord/.gitignore b/activerecord/.gitignore
new file mode 100644
index 0000000000..884ee009eb
--- /dev/null
+++ b/activerecord/.gitignore
@@ -0,0 +1,3 @@
+/sqlnet.log
+/test/config.yml
+/test/fixtures/*.sqlite*
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index b421fedc96..36a3d59784 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,242 +1,44 @@
-* Add new error class `TransactionTimeout` for MySQL adapter which will be raised
- when lock wait time expires.
+* Ensure `Associations::CollectionAssociation#size` and `Associations::CollectionAssociation#empty?`
+ use loaded association ids if present.
- *Gabriel Courtemanche*
+ *Graham Turner*
-* Remove deprecated `#migration_keys`.
+* Add support to preload associations of polymorphic associations when not all the records have the requested associations.
- *Ryuta Kamizono*
+ *Dana Sherson*
-* Automatically guess the inverse associations for STI.
+* Add `touch_all` method to `ActiveRecord::Relation`.
- *Yuichiro Kaneko*
+ Example:
-* Ensure `sum` honors `distinct` on `has_many :through` associations
+ Person.where(name: "David").touch_all(time: Time.new(2020, 5, 16, 0, 0, 0))
- Fixes #16791.
+ *fatkodima*, *duggiefresh*
- *Aaron Wortham*
+* Add `ActiveRecord::Base.base_class?` predicate.
-* Add `binary` fixture helper method.
+ *Bogdan Gusiev*
- *Atsushi Yoshida*
+* Add custom prefix option to ActiveRecord::Store.store_accessor.
-* When using `Relation#or`, extract the common conditions and put them before the OR condition.
+ *Tan Huynh*
- *Maxime Handfield Lapointe*
+* Rails 6 requires Ruby 2.4.1 or newer.
-* `Relation#or` now accepts two relations who have different values for
- `references` only, as `references` can be implicitly called by `where`.
+ *Jeremy Daer*
- Fixes #29411.
+* Deprecate `update_attributes`/`!` in favor of `update`/`!`.
- *Sean Griffin*
+ *Eddie Lebow*
-* `ApplicationRecord` is no longer generated when generating models. If you
- need to generate it, it can be created with `rails g application_record`.
-
- *Lisa Ugray*
-
-* Fix `COUNT(DISTINCT ...)` with `ORDER BY` and `LIMIT` to keep the existing select list.
-
- *Ryuta Kamizono*
-
-* When a `has_one` association is destroyed by `dependent: destroy`,
- `destroyed_by_association` will now be set to the reflection, matching the
- behaviour of `has_many` associations.
-
- *Lisa Ugray*
-
-* Fix `unscoped(where: [columns])` removing the wrong bind values
-
- When the `where` is called on a relation after a `or`, unscoping the column of that later `where` removed
- bind values used by the `or` instead. (possibly other cases too)
-
- ```
- Post.where(id: 1).or(Post.where(id: 2)).where(foo: 3).unscope(where: :foo).to_sql
- # Currently:
- # SELECT "posts".* FROM "posts" WHERE ("posts"."id" = 2 OR "posts"."id" = 3)
- # With fix:
- # SELECT "posts".* FROM "posts" WHERE ("posts"."id" = 1 OR "posts"."id" = 2)
- ```
-
- *Maxime Handfield Lapointe*
-
-* Values constructed using multi-parameter assignment will now use the
- post-type-cast value for rendering in single-field form inputs.
-
- *Sean Griffin*
-
-* `Relation#joins` is no longer affected by the target model's
- `current_scope`, with the exception of `unscoped`.
-
- Fixes #29338.
-
- *Sean Griffin*
-
-* Change sqlite3 boolean serialization to use 1 and 0
-
- SQLite natively recognizes 1 and 0 as true and false, but does not natively
- recognize 't' and 'f' as was previously serialized.
-
- This change in serialization requires a migration of stored boolean data
- for SQLite databases, so it's implemented behind a configuration flag
- whose default false value is deprecated.
-
- *Lisa Ugray*
-
-* Skip query caching when working with batches of records (`find_each`, `find_in_batches`,
- `in_batches`).
-
- Previously, records would be fetched in batches, but all records would be retained in memory
- until the end of the request or job.
-
- *Eugene Kenny*
-
-* Prevent errors raised by `sql.active_record` notification subscribers from being converted into
- `ActiveRecord::StatementInvalid` exceptions.
-
- *Dennis Taylor*
-
-* Fix eager loading/preloading association with scope including joins.
-
- Fixes #28324.
-
- *Ryuta Kamizono*
-
-* Fix transactions to apply state to child transactions
-
- Previously, if you had a nested transaction and the outer transaction was rolledback, the record from the
- inner transaction would still be marked as persisted.
-
- This change fixes that by applying the state of the parent transaction to the child transaction when the
- parent transaction is rolledback. This will correctly mark records from the inner transaction as not persisted.
-
- *Eileen M. Uchitelle*, *Aaron Patterson*
-
-* Deprecate `set_state` method in `TransactionState`
-
- Deprecated the `set_state` method in favor of setting the state via specific methods. If you need to mark the
- state of the transaction you can now use `rollback!`, `commit!` or `nullify!` instead of
- `set_state(:rolledback)`, `set_state(:committed)`, or `set_state(nil)`.
-
- *Eileen M. Uchitelle*, *Aaron Patterson*
-
-* Deprecate delegating to `arel` in `Relation`.
-
- *Ryuta Kamizono*
-
-* Fix eager loading to respect `store_full_sti_class` setting.
-
- *Ryuta Kamizono*
-
-* Query cache was unavailable when entering the `ActiveRecord::Base.cache` block
- without being connected.
-
- *Tsukasa Oishi*
-
-* Previously, when building records using a `has_many :through` association,
- if the child records were deleted before the parent was saved, they would
- still be persisted. Now, if child records are deleted before the parent is saved
- on a `has_many :through` association, the child records will not be persisted.
-
- *Tobias Kraze*
-
-* Merging two relations representing nested joins no longer transforms the joins of
- the merged relation into LEFT OUTER JOIN. Example to clarify:
-
- ```
- Author.joins(:posts).merge(Post.joins(:comments))
- # Before the change:
- #=> SELECT ... FROM authors INNER JOIN posts ON ... LEFT OUTER JOIN comments ON...
-
- # After the change:
- #=> SELECT ... FROM authors INNER JOIN posts ON ... INNER JOIN comments ON...
- ```
-
- TODO: Add to the Rails 5.2 upgrade guide
-
- *Maxime Handfield Lapointe*
-
-* `ActiveRecord::Persistence#touch` does not work well when optimistic locking enabled and
- `locking_column`, without default value, is null in the database.
-
- *bogdanvlviv*
-
-* Fix destroying existing object does not work well when optimistic locking enabled and
- `locking_column` is null in the database.
-
- *bogdanvlviv*
-
-* Use bulk INSERT to insert fixtures for better performance.
-
- *Kir Shatrov*
-
-* Prevent creation of bind param if casted value is nil.
-
- *Ryuta Kamizono*
-
-* Deprecate passing arguments and block at the same time to `count` and `sum` in `ActiveRecord::Calculations`.
-
- *Ryuta Kamizono*
-
-* Loading model schema from database is now thread-safe.
-
- Fixes #28589.
-
- *Vikrant Chaudhary*, *David Abdemoulaie*
-
-* Add `ActiveRecord::Base#cache_version` to support recyclable cache keys via the new versioned entries
- in `ActiveSupport::Cache`. This also means that `ActiveRecord::Base#cache_key` will now return a stable key
- that does not include a timestamp any more.
-
- NOTE: This feature is turned off by default, and `#cache_key` will still return cache keys with timestamps
- until you set `ActiveRecord::Base.cache_versioning = true`. That's the setting for all new apps on Rails 5.2+
+* Add ActiveRecord::Base.create_or_find_by/! to deal with the SELECT/INSERT race condition in
+ ActiveRecord::Base.find_or_create_by/! by leaning on unique constraints in the database.
*DHH*
-* Respect `SchemaDumper.ignore_tables` in rake tasks for databases structure dump
-
- *Rusty Geldmacher*, *Guillermo Iguaran*
-
-* Add type caster to `RuntimeReflection#alias_name`
-
- Fixes #28959.
-
- *Jon Moss*
-
-* Deprecate `supports_statement_cache?`.
-
- *Ryuta Kamizono*
-
-* Quote database name in `db:create` grant statement (when database user does not have access to create the database).
-
- *Rune Philosof*
-
-* Raise error `UnknownMigrationVersionError` on the movement of migrations
- when the current migration does not exist.
+* Add `Relation#pick` as short-hand for single-value plucks.
- *bogdanvlviv*
-
-* Fix `bin/rails db:forward` first migration.
-
- *bogdanvlviv*
-
-* Support Descending Indexes for MySQL.
-
- MySQL 8.0.1 and higher supports descending indexes: `DESC` in an index definition is no longer ignored.
- See https://dev.mysql.com/doc/refman/8.0/en/descending-indexes.html.
-
- *Ryuta Kamizono*
-
-* Fix inconsistency with changed attributes when overriding AR attribute reader.
-
- *bogdanvlviv*
-
-* When calling the dynamic fixture accessor method with no arguments, it now returns all fixtures of this type.
- Previously this method always returned an empty array.
-
- *Kevin McPhillips*
+ *DHH*
-Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activerecord/CHANGELOG.md) for previous changes.
+Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activerecord/CHANGELOG.md) for previous changes.
diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE
index f9e4444f07..04ba107c48 100644
--- a/activerecord/MIT-LICENSE
+++ b/activerecord/MIT-LICENSE
@@ -1,4 +1,6 @@
-Copyright (c) 2004-2017 David Heinemeier Hansson
+Copyright (c) 2004-2018 David Heinemeier Hansson
+
+Arel originally copyright (c) 2007-2016 Nick Kallen, Bryan Helmkamp, Emilio Tagua, Aaron Patterson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc
index ae53ecd177..19650b82ae 100644
--- a/activerecord/README.rdoc
+++ b/activerecord/README.rdoc
@@ -26,7 +26,7 @@ The Product class is automatically mapped to the table named "products",
which might look like this:
CREATE TABLE products (
- id int NOT NULL auto_increment,
+ id bigint NOT NULL auto_increment,
name varchar(255),
PRIMARY KEY (id)
);
@@ -208,7 +208,7 @@ API documentation is at:
* http://api.rubyonrails.org
-Bug reports can be filed for the Ruby on Rails project here:
+Bug reports for the Ruby on Rails project can be filed here:
* https://github.com/rails/rails/issues
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 57c82bf469..170c95b827 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -41,9 +41,11 @@ namespace :test do
end
end
-desc "Build MySQL and PostgreSQL test databases"
namespace :db do
+ desc "Build MySQL and PostgreSQL test databases"
task create: ["db:mysql:build", "db:postgresql:build"]
+
+ desc "Drop MySQL and PostgreSQL test databases"
task drop: ["db:mysql:drop", "db:postgresql:drop"]
end
@@ -68,7 +70,7 @@ end
(Dir["test/cases/**/*_test.rb"].reject {
|x| x.include?("/adapters/")
} + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file|
- sh(Gem.ruby, "-w" , "-Itest", file)
+ sh(Gem.ruby, "-w", "-Itest", file)
end || raise("Failures")
end
end
diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec
index 7ad06fe840..a857d00c05 100644
--- a/activerecord/activerecord.gemspec
+++ b/activerecord/activerecord.gemspec
@@ -9,7 +9,7 @@ Gem::Specification.new do |s|
s.summary = "Object-relational mapper framework (part of Rails)."
s.description = "Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in."
- s.required_ruby_version = ">= 2.2.2"
+ s.required_ruby_version = ">= 2.4.1"
s.license = "MIT"
@@ -30,6 +30,4 @@ Gem::Specification.new do |s|
s.add_dependency "activesupport", version
s.add_dependency "activemodel", version
-
- s.add_dependency "arel", "9.0.0.alpha"
end
diff --git a/activerecord/bin/test b/activerecord/bin/test
index 83c192531e..9ecf27ce67 100755
--- a/activerecord/bin/test
+++ b/activerecord/bin/test
@@ -1,6 +1,12 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
+adapter_index = ARGV.index("--adapter") || ARGV.index("-a")
+if adapter_index
+ ARGV.delete_at(adapter_index)
+ ENV["ARCONN"] = ARGV.delete_at(adapter_index).strip
+end
+
COMPONENT_ROOT = File.expand_path("..", __dir__)
require_relative "../../tools/test"
@@ -17,4 +23,5 @@ module Minitest
end
end
+Minitest.load_plugins
Minitest.extensions.unshift "active_record"
diff --git a/activerecord/examples/.gitignore b/activerecord/examples/.gitignore
deleted file mode 100644
index 0dfc1cb7fb..0000000000
--- a/activerecord/examples/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-performance.sql
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 0c19fed9e1..d198466dbf 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
#--
-# Copyright (c) 2004-2017 David Heinemeier Hansson
+# Copyright (c) 2004-2018 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -27,19 +27,20 @@ require "active_support"
require "active_support/rails"
require "active_model"
require "arel"
+require "yaml"
-require_relative "active_record/version"
-require_relative "active_record/attribute_set"
+require "active_record/version"
+require "active_model/attribute_set"
module ActiveRecord
extend ActiveSupport::Autoload
- autoload :Attribute
autoload :Base
autoload :Callbacks
autoload :Core
autoload :ConnectionHandling
autoload :CounterCache
+ autoload :DatabaseConfigurations
autoload :DynamicMatchers
autoload :Enum
autoload :InternalMetadata
@@ -104,6 +105,7 @@ module ActiveRecord
autoload :Result
autoload :TableMetadata
+ autoload :Type
end
module Coders
@@ -162,6 +164,7 @@ module ActiveRecord
"active_record/tasks/postgresql_database_tasks"
end
+ autoload :TestDatabases, "active_record/test_databases"
autoload :TestFixtures, "active_record/fixtures"
def self.eager_load!
@@ -181,3 +184,7 @@ end
ActiveSupport.on_load(:i18n) do
I18n.load_path << File.expand_path("active_record/locale/en.yml", __dir__)
end
+
+YAML.load_tags["!ruby/object:ActiveRecord::AttributeSet"] = "ActiveModel::AttributeSet"
+YAML.load_tags["!ruby/object:ActiveRecord::Attribute::FromDatabase"] = "ActiveModel::Attribute::FromDatabase"
+YAML.load_tags["!ruby/object:ActiveRecord::LazyAttributeHash"] = "ActiveModel::LazyAttributeHash"
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index e5e89734d2..27a641f05b 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -35,7 +35,7 @@ module ActiveRecord
# the database).
#
# class Customer < ActiveRecord::Base
- # composed_of :balance, class_name: "Money", mapping: %w(amount currency)
+ # composed_of :balance, class_name: "Money", mapping: %w(balance amount)
# composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
# end
#
@@ -177,9 +177,9 @@ module ActiveRecord
#
# Once a #composed_of relationship is specified for a model, records can be loaded from the database
# by specifying an instance of the value object in the conditions hash. The following example
- # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD":
+ # finds all customers with +address_street+ equal to "May Street" and +address_city+ equal to "Chicago":
#
- # Customer.where(balance: Money.new(20, "USD"))
+ # Customer.where(address: Address.new("May Street", "Chicago"))
#
module ClassMethods
# Adds reader and writer methods for manipulating a value object:
@@ -212,8 +212,7 @@ module ActiveRecord
#
# Option examples:
# composed_of :temperature, mapping: %w(reading celsius)
- # composed_of :balance, class_name: "Money", mapping: %w(balance amount),
- # converter: Proc.new { |balance| balance.to_money }
+ # composed_of :balance, class_name: "Money", mapping: %w(balance amount)
# composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
# composed_of :gps_location
# composed_of :gps_location, allow_nil: true
diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb
index 2b0b2864bc..403667fb70 100644
--- a/activerecord/lib/active_record/association_relation.rb
+++ b/activerecord/lib/active_record/association_relation.rb
@@ -2,8 +2,8 @@
module ActiveRecord
class AssociationRelation < Relation
- def initialize(klass, table, predicate_builder, association)
- super(klass, table, predicate_builder)
+ def initialize(klass, association)
+ super(klass)
@association = association
end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index ef26f4a20c..0e68e49182 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -3,7 +3,7 @@
require "active_support/core_ext/enumerable"
require "active_support/core_ext/string/conversions"
require "active_support/core_ext/module/remove_method"
-require_relative "errors"
+require "active_record/errors"
module ActiveRecord
class AssociationNotFoundError < ConfigurationError #:nodoc:
@@ -140,26 +140,6 @@ module ActiveRecord
class HasOneThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc:
end
- class HasManyThroughCantAssociateNewRecords < ActiveRecordError #:nodoc:
- def initialize(owner = nil, reflection = nil)
- if owner && reflection
- super("Cannot associate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to create the has_many :through record associating them.")
- else
- super("Cannot associate new records.")
- end
- end
- end
-
- class HasManyThroughCantDissociateNewRecords < ActiveRecordError #:nodoc:
- def initialize(owner = nil, reflection = nil)
- if owner && reflection
- super("Cannot dissociate new records through '#{owner.class.name}##{reflection.name}' on '#{reflection.source_reflection.class_name rescue nil}##{reflection.source_reflection.name rescue nil}'. Both records must have an id in order to delete the has_many :through record associating them.")
- else
- super("Cannot dissociate new records.")
- end
- end
- end
-
class ThroughNestedAssociationsAreReadonly < ActiveRecordError #:nodoc:
def initialize(owner = nil, reflection = nil)
if owner && reflection
@@ -189,16 +169,6 @@ module ActiveRecord
end
end
- class ReadOnlyAssociation < ActiveRecordError #:nodoc:
- def initialize(reflection = nil)
- if reflection
- super("Cannot add to a has_many :through association. Try adding to #{reflection.through_reflection.name.inspect}.")
- else
- super("Read-only reflection error.")
- end
- end
- end
-
# This error is raised when trying to destroy a parent instance in N:1 or 1:1 associations
# (has_many, has_one) when there is at least 1 child associated instance.
# ex: if @project.tasks.size > 0, DeleteRestrictionError will be raised when trying to destroy @project
@@ -271,7 +241,7 @@ module ActiveRecord
association
end
- def association_cached?(name) # :nodoc
+ def association_cached?(name) # :nodoc:
@association_cache.key?(name)
end
@@ -483,14 +453,14 @@ module ActiveRecord
# The tables for these classes could look something like:
#
# CREATE TABLE users (
- # id int NOT NULL auto_increment,
- # account_id int default NULL,
+ # id bigint NOT NULL auto_increment,
+ # account_id bigint default NULL,
# name varchar default NULL,
# PRIMARY KEY (id)
# )
#
# CREATE TABLE accounts (
- # id int NOT NULL auto_increment,
+ # id bigint NOT NULL auto_increment,
# name varchar default NULL,
# PRIMARY KEY (id)
# )
@@ -557,9 +527,8 @@ module ActiveRecord
# has_many :birthday_events, ->(user) { where(starts_on: user.birthday) }, class_name: 'Event'
# end
#
- # Note: Joining, eager loading and preloading of these associations is not fully possible.
+ # Note: Joining, eager loading and preloading of these associations is not possible.
# These operations happen before instance creation and the scope will be called with a +nil+ argument.
- # This can lead to unexpected behavior and is deprecated.
#
# == Association callbacks
#
@@ -850,7 +819,7 @@ module ActiveRecord
# project.milestones # fetches milestones from the database
# project.milestones.size # uses the milestone cache
# project.milestones.empty? # uses the milestone cache
- # project.milestones(true).size # fetches milestones from the database
+ # project.milestones.reload.size # fetches milestones from the database
# project.milestones # uses the milestone cache
#
# == Eager loading of associations
@@ -1092,12 +1061,6 @@ module ActiveRecord
# belongs_to :dungeon, inverse_of: :evil_wizard
# end
#
- # There are limitations to <tt>:inverse_of</tt> support:
- #
- # * does not work with <tt>:through</tt> associations.
- # * does not work with <tt>:polymorphic</tt> associations.
- # * inverse associations for #belongs_to associations #has_many are ignored.
- #
# For more information, see the documentation for the +:inverse_of+ option.
#
# == Deleting from associations
@@ -1310,6 +1273,9 @@ module ActiveRecord
# Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_many
# association will use "person_id" as the default <tt>:foreign_key</tt>.
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option.
# [:foreign_type]
# Specify the column used to store the associated object's type, if this is a polymorphic
# association. By default this is guessed to be the name of the polymorphic association
@@ -1383,8 +1349,7 @@ module ActiveRecord
# <tt>:autosave</tt> to <tt>true</tt>.
# [:inverse_of]
# Specifies the name of the #belongs_to association on the associated object
- # that is the inverse of this #has_many association. Does not work in combination
- # with <tt>:through</tt> or <tt>:as</tt> options.
+ # that is the inverse of this #has_many association.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
# [:extend]
# Specifies a module or array of modules that will be extended into the association object returned.
@@ -1480,6 +1445,9 @@ module ActiveRecord
# Specify the foreign key used for the association. By default this is guessed to be the name
# of this class in lower-case and "_id" suffixed. So a Person class that makes a #has_one association
# will use "person_id" as the default <tt>:foreign_key</tt>.
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option.
# [:foreign_type]
# Specify the column used to store the associated object's type, if this is a polymorphic
# association. By default this is guessed to be the name of the polymorphic association
@@ -1495,6 +1463,9 @@ module ActiveRecord
# <tt>:primary_key</tt>, and <tt>:foreign_key</tt> are ignored, as the association uses the
# source reflection. You can only use a <tt>:through</tt> query through a #has_one
# or #belongs_to association on the join model.
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option.
# [:source]
# Specifies the source association name used by #has_one <tt>:through</tt> queries.
# Only use it if the name cannot be inferred from the association.
@@ -1515,8 +1486,7 @@ module ActiveRecord
# <tt>:autosave</tt> to <tt>true</tt>.
# [:inverse_of]
# Specifies the name of the #belongs_to association on the associated object
- # that is the inverse of this #has_one association. Does not work in combination
- # with <tt>:through</tt> or <tt>:as</tt> options.
+ # that is the inverse of this #has_one association.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
# [:required]
# When set to +true+, the association will also have its presence validated.
@@ -1601,6 +1571,9 @@ module ActiveRecord
# association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly,
# <tt>belongs_to :favorite_person, class_name: "Person"</tt> will use a foreign key
# of "favorite_person_id".
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option.
# [:foreign_type]
# Specify the column used to store the associated object's type, if this is a polymorphic
# association. By default this is guessed to be the name of the association with a "_type"
@@ -1650,8 +1623,7 @@ module ActiveRecord
# +after_commit+ and +after_rollback+ callbacks are executed.
# [:inverse_of]
# Specifies the name of the #has_one or #has_many association on the associated
- # object that is the inverse of this #belongs_to association. Does not work in
- # combination with the <tt>:polymorphic</tt> options.
+ # object that is the inverse of this #belongs_to association.
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
# [:optional]
# When set to +true+, the association will not have its presence validated.
@@ -1820,6 +1792,9 @@ module ActiveRecord
# of this class in lower-case and "_id" suffixed. So a Person class that makes
# a #has_and_belongs_to_many association to Project will use "person_id" as the
# default <tt>:foreign_key</tt>.
+ #
+ # If you are going to modify the association (rather than just read from it), then it is
+ # a good idea to set the <tt>:inverse_of</tt> option.
# [:association_foreign_key]
# Specify the foreign key used for the association on the receiving side of the association.
# By default this is guessed to be the name of the associated class in lower-case and "_id" suffixed.
@@ -1848,7 +1823,7 @@ module ActiveRecord
builder = Builder::HasAndBelongsToMany.new name, self, options
- join_model = ActiveSupport::Deprecation.silence { builder.through_model }
+ join_model = builder.through_model
const_set join_model.name, join_model
private_constant join_model.name
@@ -1877,7 +1852,7 @@ module ActiveRecord
hm_options[k] = options[k] if options.key? k
end
- ActiveSupport::Deprecation.silence { has_many name, scope, hm_options, &extension }
+ has_many name, scope, hm_options, &extension
_reflections[name.to_s].parent_reflection = habtm_reflection
end
end
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 096f016976..4f3893588e 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -6,46 +6,36 @@ module ActiveRecord
module Associations
# Keeps track of table aliases for ActiveRecord::Associations::JoinDependency
class AliasTracker # :nodoc:
- def self.create(connection, initial_table)
- aliases = Hash.new(0)
- aliases[initial_table] = 1
- new(connection, aliases)
- end
-
- def self.create_with_joins(connection, initial_table, joins)
+ def self.create(connection, initial_table, joins)
if joins.empty?
- create(connection, initial_table)
+ aliases = Hash.new(0)
else
aliases = Hash.new { |h, k|
h[k] = initial_count_for(connection, k, joins)
}
- aliases[initial_table] = 1
- new(connection, aliases)
end
+ aliases[initial_table] = 1
+ new(connection, aliases)
end
def self.initial_count_for(connection, name, table_joins)
- # quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
- quoted_name = connection.quote_table_name(name).downcase
+ quoted_name = nil
counts = table_joins.map do |join|
if join.is_a?(Arel::Nodes::StringJoin)
+ # quoted_name should be case ignored as some database adapters (Oracle) return quoted name in uppercase
+ quoted_name ||= connection.quote_table_name(name)
+
# Table names + table aliases
- join.left.downcase.scan(
- /join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
+ join.left.scan(
+ /JOIN(?:\s+\w+)?\s+(?:\S+\s+)?(?:#{quoted_name}|#{name})\sON/i
).size
- elsif join.respond_to? :left
- join.left.table_name == name ? 1 : 0
+ elsif join.is_a?(Arel::Nodes::Join)
+ join.left.name == name ? 1 : 0
+ elsif join.is_a?(Hash)
+ join.fetch(name, 0)
else
- # this branch is reached by two tests:
- #
- # activerecord/test/cases/associations/cascaded_eager_loading_test.rb:37
- # with :posts
- #
- # activerecord/test/cases/associations/eager_test.rb:1133
- # with :comments
- #
- 0
+ raise ArgumentError, "joins list should be initialized by list of Arel::Nodes::Join"
end
end
@@ -79,10 +69,7 @@ module ActiveRecord
end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
- attr_reader :aliases
+ attr_reader :aliases
private
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index ca1f9f1650..ca8c7794e0 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -124,7 +124,7 @@ module ActiveRecord
# Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
# through association's scope)
def target_scope
- AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all)
+ AssociationRelation.create(klass, self).merge!(klass.all)
end
def extensions
@@ -156,9 +156,9 @@ module ActiveRecord
reset
end
- # We can't dump @reflection since it contains the scope proc
+ # We can't dump @reflection and @through_reflection since it contains the scope proc
def marshal_dump
- ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] }
+ ivars = (instance_variables - [:@reflection, :@through_reflection]).map { |name| [name, instance_variable_get(name)] }
[@reflection.name, ivars]
end
@@ -201,8 +201,8 @@ module ActiveRecord
if (reflection.has_one? || reflection.collection?) && !options[:through]
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
- if reflection.options[:as]
- attributes[reflection.type] = owner.class.base_class.name
+ if reflection.type
+ attributes[reflection.type] = owner.class.polymorphic_name
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index b2dc044312..0a90a6104a 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -23,8 +23,7 @@ module ActiveRecord
reflection = association.reflection
scope = klass.unscoped
owner = association.owner
- alias_tracker = AliasTracker.create(klass.connection, klass.table_name)
- chain = get_chain(reflection, association, alias_tracker)
+ chain = get_chain(reflection, association, scope.alias_tracker)
scope.extending! reflection.extensions
add_constraints(scope, owner, chain)
@@ -36,24 +35,20 @@ module ActiveRecord
binds << last_reflection.join_id_for(owner)
if last_reflection.type
- binds << owner.class.base_class.name
+ binds << owner.class.polymorphic_name
end
chain.each_cons(2).each do |reflection, next_reflection|
if reflection.type
- binds << next_reflection.klass.base_class.name
+ binds << next_reflection.klass.polymorphic_name
end
end
binds
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :value_transformation
- private
def join(table, constraint)
table.create_join(table, table.create_on(constraint))
end
@@ -68,7 +63,7 @@ module ActiveRecord
scope = apply_scope(scope, table, key, value)
if reflection.type
- polymorphic_type = transform_value(owner.class.base_class.name)
+ polymorphic_type = transform_value(owner.class.polymorphic_name)
scope = apply_scope(scope, table, reflection.type, polymorphic_type)
end
@@ -89,7 +84,7 @@ module ActiveRecord
constraint = table[key].eq(foreign_table[foreign_key])
if reflection.type
- value = transform_value(next_reflection.klass.base_class.name)
+ value = transform_value(next_reflection.klass.polymorphic_name)
scope = apply_scope(scope, table, reflection.type, value)
end
@@ -136,7 +131,7 @@ module ActiveRecord
item = eval_scope(reflection, scope_chain_item, owner)
if scope_chain_item == chain_head.scope
- scope.merge! item.except(:where, :includes)
+ scope.merge! item.except(:where, :includes, :unscope, :order)
end
reflection.all_includes do
@@ -145,7 +140,7 @@ module ActiveRecord
scope.unscope!(*item.unscope_values)
scope.where_clause += item.where_clause
- scope.order_values |= item.order_values
+ scope.order_values = item.order_values | scope.order_values
end
end
diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb
index ba54cd8f49..1109fee462 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -5,24 +5,35 @@ module ActiveRecord
# = Active Record Belongs To Association
class BelongsToAssociation < SingularAssociation #:nodoc:
def handle_dependency
- target.send(options[:dependent]) if load_target
+ return unless load_target
+
+ case options[:dependent]
+ when :destroy
+ target.destroy
+ raise ActiveRecord::Rollback unless target.destroyed?
+ else
+ target.send(options[:dependent])
+ end
end
def replace(record)
if record
raise_on_type_mismatch!(record)
update_counters_on_replace(record)
- replace_keys(record)
set_inverse_instance(record)
@updated = true
else
decrement_counters
- remove_keys
end
self.target = record
end
+ def target=(record)
+ replace_keys(record)
+ super
+ end
+
def default(&block)
writer(owner.instance_exec(&block)) if reader.nil?
end
@@ -49,9 +60,9 @@ module ActiveRecord
def update_counters(by)
if require_counter_update? && foreign_key_present?
if target && !stale_target?
- target.increment!(reflection.counter_cache_column, by)
+ target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch])
else
- klass.update_counters(target_id, reflection.counter_cache_column => by)
+ klass.update_counters(target_id, reflection.counter_cache_column => by, touch: reflection.options[:touch])
end
end
end
@@ -78,11 +89,8 @@ module ActiveRecord
end
def replace_keys(record)
- owner[reflection.foreign_key] = record._read_attribute(reflection.association_primary_key(record.class))
- end
-
- def remove_keys
- owner[reflection.foreign_key] = nil
+ owner[reflection.foreign_key] = record ?
+ record._read_attribute(reflection.association_primary_key(record.class)) : nil
end
def foreign_key_present?
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index 4ce3474bd5..75b4c4481a 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -13,12 +13,7 @@ module ActiveRecord
def replace_keys(record)
super
- owner[reflection.foreign_type] = record.class.base_class.name
- end
-
- def remove_keys
- super
- owner[reflection.foreign_type] = nil
+ owner[reflection.foreign_type] = record ? record.class.polymorphic_name : nil
end
def different_target?(record)
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index ca3032d967..7c69cd65ee 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -104,8 +104,8 @@ module ActiveRecord::Associations::Builder # :nodoc:
def self.define_readers(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{name}(*args)
- association(:#{name}).reader(*args)
+ def #{name}
+ association(:#{name}).reader
end
CODE
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 9904ee4bed..c161454c1a 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -114,9 +114,13 @@ module ActiveRecord::Associations::Builder # :nodoc:
BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method)
}}
- model.after_save callback.(:saved_changes), if: :saved_changes?
- model.after_touch callback.(:changes_to_save)
- model.after_destroy callback.(:changes_to_save)
+ unless reflection.counter_cache_column
+ model.after_create callback.(:saved_changes), if: :saved_changes?
+ model.after_destroy callback.(:changes_to_save)
+ end
+
+ model.after_update callback.(:saved_changes), if: :saved_changes?
+ model.after_touch callback.(:changes_to_save)
end
def self.add_default_callbacks(model, reflection)
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index 753fde5146..35a72c3850 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "../../associations"
+require "active_record/associations"
module ActiveRecord::Associations::Builder # :nodoc:
class CollectionAssociation < Association #:nodoc:
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index 12fcfbcd45..1981da11a2 100644
--- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -47,7 +47,7 @@ module ActiveRecord::Associations::Builder # :nodoc:
habtm = JoinTableResolver.build lhs_model, association_name, options
join_model = Class.new(ActiveRecord::Base) {
- class << self;
+ class << self
attr_accessor :left_model
attr_accessor :name
attr_accessor :table_name_resolver
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index ceedf150e3..d61d105544 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -45,6 +45,8 @@ module ActiveRecord
def ids_reader
if loaded?
target.pluck(reflection.association_primary_key)
+ elsif !target.empty?
+ load_target.pluck(reflection.association_primary_key)
else
@association_ids ||= scope.pluck(reflection.association_primary_key)
end
@@ -52,11 +54,11 @@ module ActiveRecord
# Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items
def ids_writer(ids)
- pk_type = reflection.association_primary_key_type
+ primary_key = reflection.association_primary_key
+ pk_type = klass.type_for_attribute(primary_key)
ids = Array(ids).reject(&:blank?)
ids.map! { |i| pk_type.cast(i) }
- primary_key = reflection.association_primary_key
records = klass.where(primary_key => ids).index_by do |r|
r.public_send(primary_key)
end.values_at(*ids).compact
@@ -79,7 +81,13 @@ module ActiveRecord
def find(*args)
if options[:inverse_of] && loaded?
args_flatten = args.flatten
- raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank?
+ model = scope.klass
+
+ if args_flatten.blank?
+ error_message = "Couldn't find #{model.name} without an ID"
+ raise RecordNotFound.new(error_message, model.name, model.primary_key, args)
+ end
+
result = find_by_scan(*args)
result_size = Array(result).size
@@ -181,8 +189,6 @@ module ActiveRecord
# are actually removed from the database, that depends precisely on
# +delete_records+. They are in any case removed from the collection.
def delete(*records)
- return if records.empty?
- records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) }
delete_or_destroy(records, options[:dependent])
end
@@ -192,8 +198,6 @@ module ActiveRecord
# Note that this method removes records from the database ignoring the
# +:dependent+ option.
def destroy(*records)
- return if records.empty?
- records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) }
delete_or_destroy(records, :destroy)
end
@@ -210,9 +214,11 @@ module ActiveRecord
def size
if !find_target? || loaded?
target.size
+ elsif @association_ids
+ @association_ids.size
elsif !association_scope.group_values.empty?
load_target.size
- elsif !association_scope.distinct_value && target.is_a?(Array)
+ elsif !association_scope.distinct_value && !target.empty?
unsaved_records = target.select(&:new_record?)
unsaved_records.size + count_records
else
@@ -229,10 +235,10 @@ module ActiveRecord
# loaded and you are going to fetch the records anyway it is better to
# check <tt>collection.length.zero?</tt>.
def empty?
- if loaded?
+ if loaded? || @association_ids
size.zero?
else
- @target.blank? && !scope.exists?
+ target.empty? && !scope.exists?
end
end
@@ -376,6 +382,8 @@ module ActiveRecord
end
def delete_or_destroy(records, method)
+ return if records.empty?
+ records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) }
records = records.flatten
records.each { |record| raise_on_type_mismatch!(record) }
existing_records = records.reject(&:new_record?)
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 412e89255d..9a30198b95 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -32,7 +32,7 @@ module ActiveRecord
class CollectionProxy < Relation
def initialize(klass, association) #:nodoc:
@association = association
- super klass, klass.arel_table, klass.predicate_builder
+ super klass
extensions = association.extensions
extend(*extensions) if extensions.any?
@@ -988,6 +988,12 @@ module ActiveRecord
load_target == other
end
+ ##
+ # :method: to_ary
+ #
+ # :call-seq:
+ # to_ary()
+ #
# Returns a new array of objects from the collection. If the collection
# hasn't been loaded, it fetches the records from the database.
#
@@ -1021,10 +1027,6 @@ module ActiveRecord
# # #<Pet id: 5, name: "Brain", person_id: 1>,
# # #<Pet id: 6, name: "Boss", person_id: 1>
# # ]
- def to_ary
- load_target.dup
- end
- alias_method :to_a, :to_ary
def records # :nodoc:
load_target
@@ -1072,7 +1074,6 @@ module ActiveRecord
end
# Reloads the collection from the database. Returns +self+.
- # Equivalent to <tt>collection(true)</tt>.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -1086,9 +1087,6 @@ module ActiveRecord
#
# person.pets.reload # fetches pets from the database
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
- #
- # person.pets(true) # fetches pets from the database
- # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
def reload
proxy_association.reload
reset_scope
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 07c7f28d2d..cf85a87fa7 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -63,7 +63,7 @@ module ActiveRecord
count = if reflection.has_cached_counter?
owner._read_attribute(reflection.counter_cache_column).to_i
else
- scope.count
+ scope.count(:all)
end
# If there's nothing in the database and @target has no new records
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index adbf52b87c..59929b8c4e 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -8,9 +8,7 @@ module ActiveRecord
def initialize(owner, reflection)
super
-
- @through_records = {}
- @through_association = nil
+ @through_records = {}
end
def concat(*records)
@@ -50,11 +48,6 @@ module ActiveRecord
end
private
-
- def through_association
- @through_association ||= owner.association(through_reflection.name)
- end
-
# The through record (built with build_record) is temporarily cached
# so that it may be reused if insert_record is subsequently called.
#
@@ -140,21 +133,15 @@ module ActiveRecord
scope = through_association.scope
scope.where! construct_join_attributes(*records)
+ scope = scope.where(through_scope_attributes)
case method
when :destroy
if scope.klass.primary_key
- count = scope.destroy_all.length
+ count = scope.destroy_all.count(&:destroyed?)
else
scope.each(&:_run_destroy_callbacks)
-
- arel = scope.arel
-
- stmt = Arel::DeleteManager.new
- stmt.from scope.klass.arel_table
- stmt.wheres = arel.constraints
-
- count = scope.klass.connection.delete(stmt, "SQL")
+ count = scope.delete_all
end
when :nullify
count = scope.update_all(source_reflection.foreign_key => nil)
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 7953b89f61..090b082cb0 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -60,6 +60,7 @@ module ActiveRecord
when :destroy
target.destroyed_by_association = reflection
target.destroy
+ throw(:abort) unless target.destroyed?
when :nullify
target.update_columns(reflection.foreign_key => nil) if target.persisted?
end
diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb
index 36746f9115..019bf0729f 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -6,17 +6,16 @@ module ActiveRecord
class HasOneThroughAssociation < HasOneAssociation #:nodoc:
include ThroughAssociation
- def replace(record)
- create_through_record(record)
+ def replace(record, save = true)
+ create_through_record(record, save)
self.target = record
end
private
-
- def create_through_record(record)
+ def create_through_record(record, save)
ensure_not_nested
- through_proxy = owner.association(through_reflection.name)
+ through_proxy = through_association
through_record = through_proxy.load_target
if through_record && !record
@@ -29,8 +28,12 @@ module ActiveRecord
end
if through_record
- through_record.update(attributes)
- elsif owner.new_record?
+ if through_record.new_record?
+ through_record.assign_attributes(attributes)
+ else
+ through_record.update(attributes)
+ end
+ elsif owner.new_record? || !save
through_proxy.build(attributes)
else
through_proxy.create(attributes)
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index 77b3d11b47..f88e383fe0 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -43,8 +43,6 @@ module ActiveRecord
Column = Struct.new(:name, :alias)
end
- attr_reader :alias_tracker, :base_klass, :join_root
-
def self.make_tree(associations)
hash = {}
walk_tree associations, hash
@@ -69,30 +67,8 @@ module ActiveRecord
end
end
- # base is the base class on which operation is taking place.
- # associations is the list of associations which are joined using hash, symbol or array.
- # joins is the list of all string join commands and arel nodes.
- #
- # Example :
- #
- # class Physician < ActiveRecord::Base
- # has_many :appointments
- # has_many :patients, through: :appointments
- # end
- #
- # If I execute `@physician.patients.to_a` then
- # base # => Physician
- # associations # => []
- # joins # => [#<Arel::Nodes::InnerJoin: ...]
- #
- # However if I execute `Physician.joins(:appointments).to_a` then
- # base # => Physician
- # associations # => [:appointments]
- # joins # => []
- #
- def initialize(base, table, associations, joins, eager_loading: true)
- @alias_tracker = AliasTracker.create_with_joins(base.connection, base.table_name, joins)
- @eager_loading = eager_loading
+ def initialize(base, table, associations, alias_tracker)
+ @alias_tracker = alias_tracker
tree = self.class.make_tree associations
@join_root = JoinBase.new(base, table, build(tree, base))
@join_root.children.each { |child| construct_tables! @join_root, child }
@@ -158,6 +134,9 @@ module ActiveRecord
parents.values
end
+ protected
+ attr_reader :alias_tracker, :base_klass, :join_root
+
private
def make_constraints(parent, child, tables, join_type)
@@ -220,12 +199,11 @@ module ActiveRecord
reflection.check_eager_loadable!
if reflection.polymorphic?
- next unless @eager_loading
raise EagerLoadPolymorphicError.new(reflection)
end
- JoinAssociation.new reflection, build(right, reflection.klass)
- end.compact
+ JoinAssociation.new(reflection, build(right, reflection.klass), alias_tracker)
+ end
end
def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index a526468bf6..c36386ec7e 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "join_part"
+require "active_record/associations/join_dependency/join_part"
module ActiveRecord
module Associations
@@ -11,11 +11,12 @@ module ActiveRecord
attr_accessor :tables
- def initialize(reflection, children)
+ def initialize(reflection, children, alias_tracker)
super(reflection.klass, children)
- @reflection = reflection
- @tables = nil
+ @alias_tracker = alias_tracker
+ @reflection = reflection
+ @tables = nil
end
def match?(other)
@@ -38,11 +39,12 @@ module ActiveRecord
joins << table.create_join(table, table.create_on(constraint), join_type)
join_scope = reflection.join_scope(table, foreign_klass)
+ arel = join_scope.arel(alias_tracker.aliases)
- if join_scope.arel.constraints.any?
- joins.concat join_scope.arel.join_sources
+ if arel.constraints.any?
+ joins.concat arel.join_sources
right = joins.last.right
- right.expr = right.expr.and(join_scope.arel.constraints)
+ right.expr = right.expr.and(arel.constraints)
end
# The current table in this iteration becomes the foreign table in the next
@@ -55,6 +57,9 @@ module ActiveRecord
def table
tables.first
end
+
+ private
+ attr_reader :alias_tracker
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
index 8a8fa8993b..988b4e8fa2 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "join_part"
+require "active_record/associations/join_dependency/join_part"
module ActiveRecord
module Associations
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index e1754d4a19..5c2ac5b374 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -44,16 +44,8 @@ module ActiveRecord
extend ActiveSupport::Autoload
eager_autoload do
- autoload :Association, "active_record/associations/preloader/association"
- autoload :SingularAssociation, "active_record/associations/preloader/singular_association"
- autoload :CollectionAssociation, "active_record/associations/preloader/collection_association"
- autoload :ThroughAssociation, "active_record/associations/preloader/through_association"
-
- autoload :HasMany, "active_record/associations/preloader/has_many"
- autoload :HasManyThrough, "active_record/associations/preloader/has_many_through"
- autoload :HasOne, "active_record/associations/preloader/has_one"
- autoload :HasOneThrough, "active_record/associations/preloader/has_one_through"
- autoload :BelongsTo, "active_record/associations/preloader/belongs_to"
+ autoload :Association, "active_record/associations/preloader/association"
+ autoload :ThroughAssociation, "active_record/associations/preloader/through_association"
end
# Eager loads the named associations for the given Active Record record(s).
@@ -91,7 +83,7 @@ module ActiveRecord
# { author: :avatar }
# [ :books, { author: :avatar } ]
def preload(records, associations, preload_scope = nil)
- records = records.compact
+ records = Array.wrap(records).compact
if records.empty?
[]
@@ -106,26 +98,30 @@ module ActiveRecord
private
# Loads all the given data into +records+ for the +association+.
- def preloaders_on(association, records, scope)
+ def preloaders_on(association, records, scope, polymorphic_parent = false)
case association
when Hash
- preloaders_for_hash(association, records, scope)
+ preloaders_for_hash(association, records, scope, polymorphic_parent)
when Symbol
- preloaders_for_one(association, records, scope)
+ preloaders_for_one(association, records, scope, polymorphic_parent)
when String
- preloaders_for_one(association.to_sym, records, scope)
+ preloaders_for_one(association.to_sym, records, scope, polymorphic_parent)
else
raise ArgumentError, "#{association.inspect} was not recognized for preload"
end
end
- def preloaders_for_hash(association, records, scope)
+ def preloaders_for_hash(association, records, scope, polymorphic_parent)
association.flat_map { |parent, child|
- loaders = preloaders_for_one parent, records, scope
+ loaders = preloaders_for_one parent, records, scope, polymorphic_parent
recs = loaders.flat_map(&:preloaded_records).uniq
+
+ reflection = records.first.class._reflect_on_association(parent)
+ polymorphic_parent = reflection && reflection.options[:polymorphic]
+
loaders.concat Array.wrap(child).flat_map { |assoc|
- preloaders_on assoc, recs, scope
+ preloaders_on assoc, recs, scope, polymorphic_parent
}
loaders
}
@@ -143,8 +139,8 @@ module ActiveRecord
# Additionally, polymorphic belongs_to associations can have multiple associated
# classes, depending on the polymorphic_type field. So we group by the classes as
# well.
- def preloaders_for_one(association, records, scope)
- grouped_records(association, records).flat_map do |reflection, klasses|
+ def preloaders_for_one(association, records, scope, polymorphic_parent)
+ grouped_records(association, records, polymorphic_parent).flat_map do |reflection, klasses|
klasses.map do |rhs_klass, rs|
loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope)
loader.run self
@@ -153,10 +149,11 @@ module ActiveRecord
end
end
- def grouped_records(association, records)
+ def grouped_records(association, records, polymorphic_parent)
h = {}
records.each do |record|
next unless record
+ next if polymorphic_parent && !record.class._reflect_on_association(association)
assoc = record.association(association)
next unless assoc.klass
klasses = h[assoc.reflection] ||= {}
@@ -166,8 +163,6 @@ module ActiveRecord
end
class AlreadyLoaded # :nodoc:
- attr_reader :owners, :reflection
-
def initialize(klass, owners, reflection, preload_scope)
@owners = owners
@reflection = reflection
@@ -178,11 +173,13 @@ module ActiveRecord
def preloaded_records
owners.flat_map { |owner| owner.association(reflection.name).target }
end
+
+ private
+ attr_reader :owners, :reflection
end
# Returns a class containing the logic needed to load preload the data
- # and attach it to a relation. For example +Preloader::Association+ or
- # +Preloader::HasManyThrough+. The class returned implements a `run` method
+ # and attach it to a relation. The class returned implements a `run` method
# that accepts a preloader.
def preloader_for(reflection, owners)
if owners.first.association(reflection.name).loaded?
@@ -190,13 +187,10 @@ module ActiveRecord
end
reflection.check_preloadable!
- case reflection.macro
- when :has_many
- reflection.options[:through] ? HasManyThrough : HasMany
- when :has_one
- reflection.options[:through] ? HasOneThrough : HasOne
- when :belongs_to
- BelongsTo
+ if reflection.options[:through]
+ ThroughAssociation
+ else
+ Association
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index fe696e0d6e..d6f7359055 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -4,7 +4,6 @@ module ActiveRecord
module Associations
class Preloader
class Association #:nodoc:
- attr_reader :owners, :reflection, :preload_scope, :model, :klass
attr_reader :preloaded_records
def initialize(klass, owners, reflection, preload_scope)
@@ -17,12 +16,20 @@ module ActiveRecord
end
def run(preloader)
- associated_records_by_owner(preloader).each do |owner, records|
- associate_records_to_owner(owner, records)
+ records = load_records do |record|
+ owner = owners_by_key[convert_key(record[association_key_name])]
+ association = owner.association(reflection.name)
+ association.set_inverse_instance(record)
+ end
+
+ owners.each do |owner|
+ associate_records_to_owner(owner, records[convert_key(owner[owner_key_name])] || [])
end
end
private
+ attr_reader :owners, :reflection, :preload_scope, :model, :klass
+
# The name of the key on the associated records
def association_key_name
reflection.join_primary_key(klass)
@@ -33,37 +40,25 @@ module ActiveRecord
reflection.join_foreign_key
end
- def associated_records_by_owner(preloader)
- records = load_records do |record|
- owner = owners_by_key[convert_key(record[association_key_name])]
- association = owner.association(reflection.name)
- association.set_inverse_instance(record)
- end
-
- owners.each_with_object({}) do |owner, result|
- result[owner] = records[convert_key(owner[owner_key_name])] || []
- end
- end
-
def associate_records_to_owner(owner, records)
- raise NotImplementedError
+ association = owner.association(reflection.name)
+ association.loaded!
+ if reflection.collection?
+ association.target.concat(records)
+ else
+ association.target = records.first unless records.empty?
+ end
end
def owner_keys
- unless defined?(@owner_keys)
- @owner_keys = owners.map do |owner|
- owner[owner_key_name]
- end
- @owner_keys.uniq!
- @owner_keys.compact!
- end
- @owner_keys
+ @owner_keys ||= owners_by_key.keys
end
def owners_by_key
unless defined?(@owners_by_key)
@owners_by_key = owners.each_with_object({}) do |owner, h|
- h[convert_key(owner[owner_key_name])] = owner
+ key = convert_key(owner[owner_key_name])
+ h[key] = owner if key
end
end
@owners_by_key
@@ -86,11 +81,11 @@ module ActiveRecord
end
def association_key_type
- @klass.type_for_attribute(association_key_name.to_s).type
+ @klass.type_for_attribute(association_key_name).type
end
def owner_key_type
- @model.type_for_attribute(owner_key_name.to_s).type
+ @model.type_for_attribute(owner_key_name).type
end
def load_records(&block)
@@ -122,7 +117,7 @@ module ActiveRecord
scope = klass.scope_for_association
if reflection.type
- scope.where!(reflection.type => model.base_class.sti_name)
+ scope.where!(reflection.type => model.polymorphic_name)
end
scope.merge!(reflection_scope) if reflection.scope
diff --git a/activerecord/lib/active_record/associations/preloader/belongs_to.rb b/activerecord/lib/active_record/associations/preloader/belongs_to.rb
deleted file mode 100644
index a8e3340b23..0000000000
--- a/activerecord/lib/active_record/associations/preloader/belongs_to.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module Associations
- class Preloader
- class BelongsTo < SingularAssociation #:nodoc:
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb
deleted file mode 100644
index fc2029f54a..0000000000
--- a/activerecord/lib/active_record/associations/preloader/collection_association.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module Associations
- class Preloader
- class CollectionAssociation < Association #:nodoc:
- private
- def associate_records_to_owner(owner, records)
- association = owner.association(reflection.name)
- association.loaded!
- association.target.concat(records)
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_many.rb b/activerecord/lib/active_record/associations/preloader/has_many.rb
deleted file mode 100644
index 72f55bc43f..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_many.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module Associations
- class Preloader
- class HasMany < CollectionAssociation #:nodoc:
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
deleted file mode 100644
index 0639fdca44..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module Associations
- class Preloader
- class HasManyThrough < CollectionAssociation #:nodoc:
- include ThroughAssociation
-
- def associated_records_by_owner(preloader)
- records_by_owner = super
-
- if reflection_scope.distinct_value
- records_by_owner.each_value(&:uniq!)
- end
-
- records_by_owner
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_one.rb b/activerecord/lib/active_record/associations/preloader/has_one.rb
deleted file mode 100644
index e339b65fb5..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_one.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module Associations
- class Preloader
- class HasOne < SingularAssociation #:nodoc:
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_one_through.rb b/activerecord/lib/active_record/associations/preloader/has_one_through.rb
deleted file mode 100644
index 17734d0257..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_one_through.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module Associations
- class Preloader
- class HasOneThrough < SingularAssociation #:nodoc:
- include ThroughAssociation
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb
deleted file mode 100644
index 30a92411e3..0000000000
--- a/activerecord/lib/active_record/associations/preloader/singular_association.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- module Associations
- class Preloader
- class SingularAssociation < Association #:nodoc:
- private
- def associate_records_to_owner(owner, records)
- association = owner.association(reflection.name)
- association.target = records.first
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index fa32cc5553..a6b7ab80a2 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -3,78 +3,54 @@
module ActiveRecord
module Associations
class Preloader
- module ThroughAssociation #:nodoc:
- def through_reflection
- reflection.through_reflection
- end
-
- def source_reflection
- reflection.source_reflection
- end
-
- def associated_records_by_owner(preloader)
- through_scope = through_scope()
-
- preloader.preload(owners,
- through_reflection.name,
- through_scope)
-
- through_records = owners.map do |owner|
- center = owner.association(through_reflection.name).target
- [owner, Array(center)]
- end
-
- reset_association(owners, through_reflection.name, through_scope)
-
- middle_records = through_records.flat_map(&:last)
-
- reflection_scope = reflection_scope() if reflection.scope
-
- preloaders = preloader.preload(middle_records,
- source_reflection.name,
- reflection_scope)
-
+ class ThroughAssociation < Association # :nodoc:
+ def run(preloader)
+ already_loaded = owners.first.association(through_reflection.name).loaded?
+ through_scope = through_scope()
+ reflection_scope = target_reflection_scope
+ through_preloaders = preloader.preload(owners, through_reflection.name, through_scope)
+ middle_records = through_preloaders.flat_map(&:preloaded_records)
+ preloaders = preloader.preload(middle_records, source_reflection.name, reflection_scope)
@preloaded_records = preloaders.flat_map(&:preloaded_records)
- middle_to_pl = preloaders.each_with_object({}) do |pl, h|
- pl.owners.each { |middle|
- h[middle] = pl
- }
- end
-
- through_records.each_with_object({}) do |(lhs, center), records_by_owner|
- pl_to_middle = center.group_by { |record| middle_to_pl[record] }
-
- records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles|
- rhs_records = middles.flat_map { |r|
- r.association(source_reflection.name).target
- }.compact
-
- # Respect the order on `reflection_scope` if it exists, else use the natural order.
- if reflection_scope && !reflection_scope.order_values.empty?
- @id_map ||= id_to_index_map @preloaded_records
- rhs_records.sort_by { |rhs| @id_map[rhs] }
- else
- rhs_records
+ owners.each do |owner|
+ through_records = Array(owner.association(through_reflection.name).target)
+ if already_loaded
+ if source_type = reflection.options[:source_type]
+ through_records = through_records.select do |record|
+ record[reflection.foreign_type] == source_type
+ end
end
+ else
+ owner.association(through_reflection.name).reset if through_scope
+ end
+ result = through_records.flat_map do |record|
+ association = record.association(source_reflection.name)
+ target = association.target
+ association.reset if preload_scope
+ target
end
+ result.compact!
+ if reflection_scope
+ result.sort_by! { |rhs| preload_index[rhs] } if reflection_scope.order_values.any?
+ result.uniq! if reflection_scope.distinct_value
+ end
+ associate_records_to_owner(owner, result)
end
end
private
+ def through_reflection
+ reflection.through_reflection
+ end
- def id_to_index_map(ids)
- id_map = {}
- ids.each_with_index { |id, index| id_map[id] = index }
- id_map
+ def source_reflection
+ reflection.source_reflection
end
- def reset_association(owners, association_name, should_reset)
- # Don't cache the association - we would only be caching a subset
- if should_reset
- owners.each { |owner|
- owner.association(association_name).reset
- }
+ def preload_index
+ @preload_index ||= @preloaded_records.each_with_object({}).with_index do |(id, result), index|
+ result[id] = index
end
end
@@ -115,6 +91,16 @@ module ActiveRecord
scope unless scope.empty_scope?
end
+
+ def target_reflection_scope
+ if preload_scope
+ reflection_scope.merge(preload_scope)
+ elsif reflection.scope
+ reflection_scope
+ else
+ nil
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index bce2a95ce1..5afb0bc068 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -4,9 +4,24 @@ module ActiveRecord
module Associations
# = Active Record Through Association
module ThroughAssociation #:nodoc:
- delegate :source_reflection, :through_reflection, to: :reflection
+ delegate :source_reflection, to: :reflection
private
+ def through_reflection
+ @through_reflection ||= begin
+ refl = reflection.through_reflection
+
+ while refl.through_reflection?
+ refl = refl.through_reflection
+ end
+
+ refl
+ end
+ end
+
+ def through_association
+ @through_association ||= owner.association(through_reflection.name)
+ end
# We merge in these scopes for two reasons:
#
@@ -38,24 +53,22 @@ module ActiveRecord
def construct_join_attributes(*records)
ensure_mutable
- if source_reflection.association_primary_key(reflection.klass) == reflection.klass.primary_key
+ association_primary_key = source_reflection.association_primary_key(reflection.klass)
+
+ if association_primary_key == reflection.klass.primary_key && !options[:source_type]
join_attributes = { source_reflection.name => records }
else
join_attributes = {
- source_reflection.foreign_key =>
- records.map { |record|
- record.send(source_reflection.association_primary_key(reflection.klass))
- }
+ source_reflection.foreign_key => records.map(&association_primary_key.to_sym)
}
end
if options[:source_type]
- join_attributes[source_reflection.foreign_type] =
- records.map { |record| record.class.base_class.name }
+ join_attributes[source_reflection.foreign_type] = [ options[:source_type] ]
end
if records.count == 1
- Hash[join_attributes.map { |k, v| [k, v.first] }]
+ join_attributes.transform_values!(&:first)
else
join_attributes
end
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
deleted file mode 100644
index fc474edc15..0000000000
--- a/activerecord/lib/active_record/attribute.rb
+++ /dev/null
@@ -1,242 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- class Attribute # :nodoc:
- class << self
- def from_database(name, value, type)
- FromDatabase.new(name, value, type)
- end
-
- def from_user(name, value, type, original_attribute = nil)
- FromUser.new(name, value, type, original_attribute)
- end
-
- def with_cast_value(name, value, type)
- WithCastValue.new(name, value, type)
- end
-
- def null(name)
- Null.new(name)
- end
-
- def uninitialized(name, type)
- Uninitialized.new(name, type)
- end
- end
-
- attr_reader :name, :value_before_type_cast, :type
-
- # This method should not be called directly.
- # Use #from_database or #from_user
- def initialize(name, value_before_type_cast, type, original_attribute = nil)
- @name = name
- @value_before_type_cast = value_before_type_cast
- @type = type
- @original_attribute = original_attribute
- end
-
- def value
- # `defined?` is cheaper than `||=` when we get back falsy values
- @value = type_cast(value_before_type_cast) unless defined?(@value)
- @value
- end
-
- def original_value
- if assigned?
- original_attribute.original_value
- else
- type_cast(value_before_type_cast)
- end
- end
-
- def value_for_database
- type.serialize(value)
- end
-
- def changed?
- changed_from_assignment? || changed_in_place?
- end
-
- def changed_in_place?
- has_been_read? && type.changed_in_place?(original_value_for_database, value)
- end
-
- def forgetting_assignment
- with_value_from_database(value_for_database)
- end
-
- def with_value_from_user(value)
- type.assert_valid_value(value)
- self.class.from_user(name, value, type, original_attribute || self)
- end
-
- def with_value_from_database(value)
- self.class.from_database(name, value, type)
- end
-
- def with_cast_value(value)
- self.class.with_cast_value(name, value, type)
- end
-
- def with_type(type)
- if changed_in_place?
- with_value_from_user(value).with_type(type)
- else
- self.class.new(name, value_before_type_cast, type, original_attribute)
- end
- end
-
- def type_cast(*)
- raise NotImplementedError
- end
-
- def initialized?
- true
- end
-
- def came_from_user?
- false
- end
-
- def has_been_read?
- defined?(@value)
- end
-
- def ==(other)
- self.class == other.class &&
- name == other.name &&
- value_before_type_cast == other.value_before_type_cast &&
- type == other.type
- end
- alias eql? ==
-
- def hash
- [self.class, name, value_before_type_cast, type].hash
- end
-
- def init_with(coder)
- @name = coder["name"]
- @value_before_type_cast = coder["value_before_type_cast"]
- @type = coder["type"]
- @original_attribute = coder["original_attribute"]
- @value = coder["value"] if coder.map.key?("value")
- end
-
- def encode_with(coder)
- coder["name"] = name
- coder["value_before_type_cast"] = value_before_type_cast unless value_before_type_cast.nil?
- coder["type"] = type if type
- coder["original_attribute"] = original_attribute if original_attribute
- coder["value"] = value if defined?(@value)
- end
-
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :original_attribute
- alias_method :assigned?, :original_attribute
-
- def original_value_for_database
- if assigned?
- original_attribute.original_value_for_database
- else
- _original_value_for_database
- end
- end
-
- private
- def initialize_dup(other)
- if defined?(@value) && @value.duplicable?
- @value = @value.dup
- end
- end
-
- def changed_from_assignment?
- assigned? && type.changed?(original_value, value, value_before_type_cast)
- end
-
- def _original_value_for_database
- type.serialize(original_value)
- end
-
- class FromDatabase < Attribute # :nodoc:
- def type_cast(value)
- type.deserialize(value)
- end
-
- def _original_value_for_database
- value_before_type_cast
- end
- end
-
- class FromUser < Attribute # :nodoc:
- def type_cast(value)
- type.cast(value)
- end
-
- def came_from_user?
- !type.value_constructed_by_mass_assignment?(value_before_type_cast)
- end
- end
-
- class WithCastValue < Attribute # :nodoc:
- def type_cast(value)
- value
- end
-
- def changed_in_place?
- false
- end
- end
-
- class Null < Attribute # :nodoc:
- def initialize(name)
- super(name, nil, Type.default_value)
- end
-
- def type_cast(*)
- nil
- end
-
- def with_type(type)
- self.class.with_cast_value(name, nil, type)
- end
-
- def with_value_from_database(value)
- raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
- end
- alias_method :with_value_from_user, :with_value_from_database
- end
-
- class Uninitialized < Attribute # :nodoc:
- UNINITIALIZED_ORIGINAL_VALUE = Object.new
-
- def initialize(name, type)
- super(name, nil, type)
- end
-
- def value
- if block_given?
- yield name
- end
- end
-
- def original_value
- UNINITIALIZED_ORIGINAL_VALUE
- end
-
- def value_for_database
- end
-
- def initialized?
- false
- end
-
- def with_type(type)
- self.class.new(name, type)
- end
- end
- private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
- end
-end
diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb
deleted file mode 100644
index 690a931615..0000000000
--- a/activerecord/lib/active_record/attribute/user_provided_default.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "../attribute"
-
-module ActiveRecord
- class Attribute # :nodoc:
- class UserProvidedDefault < FromUser # :nodoc:
- def initialize(name, value, type, database_default)
- @user_provided_value = value
- super(name, value, type, database_default)
- end
-
- def value_before_type_cast
- if user_provided_value.is_a?(Proc)
- @memoized_value_before_type_cast ||= user_provided_value.call
- else
- @user_provided_value
- end
- end
-
- def with_type(type)
- self.class.new(name, user_provided_value, type, original_attribute)
- end
-
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :user_provided_value
- end
- end
-end
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index 8b0d9aab01..b6f0e18764 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -4,14 +4,8 @@ require "active_model/forbidden_attributes_protection"
module ActiveRecord
module AttributeAssignment
- extend ActiveSupport::Concern
include ActiveModel::AttributeAssignment
- # Alias for ActiveModel::AttributeAssignment#assign_attributes. See ActiveModel::AttributeAssignment.
- def attributes=(attributes)
- assign_attributes(attributes)
- end
-
private
def _assign_attributes(attributes)
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index e4ca6c8408..83b5a5e698 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -33,7 +33,9 @@ module ActiveRecord
BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass)
- class GeneratedAttributeMethods < Module; end # :nodoc:
+ class GeneratedAttributeMethods < Module #:nodoc:
+ include Mutex_m
+ end
module ClassMethods
def inherited(child_class) #:nodoc:
@@ -42,7 +44,7 @@ module ActiveRecord
end
def initialize_generated_modules # :nodoc:
- @generated_attribute_methods = GeneratedAttributeMethods.new { extend Mutex_m }
+ @generated_attribute_methods = GeneratedAttributeMethods.new
@attribute_methods_generated = false
include @generated_attribute_methods
@@ -57,7 +59,7 @@ module ActiveRecord
# attribute methods.
generated_attribute_methods.synchronize do
return false if @attribute_methods_generated
- superclass.define_attribute_methods unless self == base_class
+ superclass.define_attribute_methods unless base_class?
super(attribute_names)
@attribute_methods_generated = true
end
@@ -165,6 +167,57 @@ module ActiveRecord
end
end
+ # Regexp whitelist. Matches the following:
+ # "#{table_name}.#{column_name}"
+ # "#{column_name}"
+ COLUMN_NAME_WHITELIST = /\A(?:\w+\.)?\w+\z/i
+
+ # Regexp whitelist. Matches the following:
+ # "#{table_name}.#{column_name}"
+ # "#{table_name}.#{column_name} #{direction}"
+ # "#{table_name}.#{column_name} #{direction} NULLS FIRST"
+ # "#{table_name}.#{column_name} NULLS LAST"
+ # "#{column_name}"
+ # "#{column_name} #{direction}"
+ # "#{column_name} #{direction} NULLS FIRST"
+ # "#{column_name} NULLS LAST"
+ COLUMN_NAME_ORDER_WHITELIST = /
+ \A
+ (?:\w+\.)?
+ \w+
+ (?:\s+asc|\s+desc)?
+ (?:\s+nulls\s+(?:first|last))?
+ \z
+ /ix
+
+ def enforce_raw_sql_whitelist(args, whitelist: COLUMN_NAME_WHITELIST) # :nodoc:
+ unexpected = args.reject do |arg|
+ arg.kind_of?(Arel::Node) ||
+ arg.is_a?(Arel::Nodes::SqlLiteral) ||
+ arg.is_a?(Arel::Attributes::Attribute) ||
+ arg.to_s.split(/\s*,\s*/).all? { |part| whitelist.match?(part) }
+ end
+
+ return if unexpected.none?
+
+ if allow_unsafe_raw_sql == :deprecated
+ ActiveSupport::Deprecation.warn(
+ "Dangerous query method (method whose arguments are used as raw " \
+ "SQL) called with non-attribute argument(s): " \
+ "#{unexpected.map(&:inspect).join(", ")}. Non-attribute " \
+ "arguments will be disallowed in Rails 6.0. This method should " \
+ "not be called with user-provided values, such as request " \
+ "parameters or model attributes. Known-safe values can be passed " \
+ "by wrapping them in Arel.sql()."
+ )
+ else
+ raise(ActiveRecord::UnknownAttributeReference,
+ "Query method called with non-attribute argument(s): " +
+ unexpected.map(&:inspect).join(", ")
+ )
+ end
+ end
+
# Returns true if the given attribute exists, otherwise false.
#
# class Person < ActiveRecord::Base
@@ -234,7 +287,7 @@ module ActiveRecord
return has_attribute?(name)
end
- return true
+ true
end
# Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
@@ -390,33 +443,24 @@ module ActiveRecord
@attributes.accessed
end
- protected
-
- def attribute_method?(attr_name) # :nodoc:
+ private
+ def attribute_method?(attr_name)
# We check defined? because Syck calls respond_to? before actually calling initialize.
defined?(@attributes) && @attributes.key?(attr_name)
end
- private
-
- def arel_attributes_with_values_for_create(attribute_names)
- arel_attributes_with_values(attributes_for_create(attribute_names))
+ def attributes_with_values_for_create(attribute_names)
+ attributes_with_values(attributes_for_create(attribute_names))
end
- def arel_attributes_with_values_for_update(attribute_names)
- arel_attributes_with_values(attributes_for_update(attribute_names))
+ def attributes_with_values_for_update(attribute_names)
+ attributes_with_values(attributes_for_update(attribute_names))
end
- # Returns a Hash of the Arel::Attributes and attribute values that have been
- # typecasted for use in an Arel insert/update method.
- def arel_attributes_with_values(attribute_names)
- attrs = {}
- arel_table = self.class.arel_table
-
- attribute_names.each do |name|
- attrs[arel_table[name]] = typecasted_attribute_value(name)
+ def attributes_with_values(attribute_names)
+ attribute_names.each_with_object({}) do |name, attrs|
+ attrs[name] = _read_attribute(name)
end
- attrs
end
# Filters the primary keys and readonly attributes from the attribute names.
@@ -441,9 +485,5 @@ module ActiveRecord
def pk_attribute?(name)
name == self.class.primary_key
end
-
- def typecasted_attribute_value(name)
- _read_attribute(name)
- end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 4f957ac3ca..7224f970e0 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "active_support/core_ext/module/attribute_accessors"
-require_relative "../attribute_mutation_tracker"
module ActiveRecord
module AttributeMethods
@@ -33,66 +32,13 @@ module ActiveRecord
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
+ @previously_changed = ActiveSupport::HashWithIndifferentAccess.new
@mutations_before_last_save = nil
- clear_mutation_trackers
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
- end
- end
-
- def initialize_dup(other) # :nodoc:
- super
- @attributes = self.class._default_attributes.map do |attr|
- attr.with_value_from_user(@attributes.fetch_value(attr.name))
- end
- clear_mutation_trackers
- end
-
- def changes_applied # :nodoc:
- @mutations_before_last_save = mutation_tracker
- @mutations_from_database = AttributeMutationTracker.new(@attributes)
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
- forget_attribute_assignments
- clear_mutation_trackers
- end
-
- def clear_changes_information # :nodoc:
- @mutations_before_last_save = nil
- @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new
- forget_attribute_assignments
- clear_mutation_trackers
- end
-
- def clear_attribute_changes(attr_names) # :nodoc:
- super
- attr_names.each do |attr_name|
- clear_attribute_change(attr_name)
- end
- end
-
- def changed_attributes # :nodoc:
- # This should only be set by methods which will call changed_attributes
- # multiple times when it is known that the computed value cannot change.
- if defined?(@cached_changed_attributes)
- @cached_changed_attributes
- else
- super.reverse_merge(mutation_tracker.changed_values).freeze
- end
- end
-
- def changes # :nodoc:
- cache_changed_attributes do
- super
+ @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new
+ @mutations_from_database = nil
end
end
- def previous_changes # :nodoc:
- mutations_before_last_save.changes
- end
-
- def attribute_changed_in_place?(attr_name) # :nodoc:
- mutation_tracker.changed_in_place?(attr_name)
- end
-
# Did this attribute change when we last saved? This method can be invoked
# as +saved_change_to_name?+ instead of <tt>saved_change_to_attribute?("name")</tt>.
# Behaves similarly to +attribute_changed?+. This method is useful in
@@ -168,7 +114,7 @@ module ActiveRecord
# Alias for +changed+
def changed_attribute_names_to_save
- changes_to_save.keys
+ mutations_from_database.changed_attribute_names
end
# Alias for +changed_attributes+
@@ -183,34 +129,6 @@ module ActiveRecord
result
end
- def mutation_tracker
- unless defined?(@mutation_tracker)
- @mutation_tracker = nil
- end
- @mutation_tracker ||= AttributeMutationTracker.new(@attributes)
- end
-
- def mutations_from_database
- unless defined?(@mutations_from_database)
- @mutations_from_database = nil
- end
- @mutations_from_database ||= mutation_tracker
- end
-
- def changes_include?(attr_name)
- super || mutation_tracker.changed?(attr_name)
- end
-
- def clear_attribute_change(attr_name)
- mutation_tracker.forget_change(attr_name)
- mutations_from_database.forget_change(attr_name)
- end
-
- def attribute_will_change!(attr_name)
- super
- mutations_from_database.force_change(attr_name)
- end
-
def _update_record(*)
partial_writes? ? super(keys_for_partial_write) : super
end
@@ -222,30 +140,6 @@ module ActiveRecord
def keys_for_partial_write
changed_attribute_names_to_save & self.class.column_names
end
-
- def forget_attribute_assignments
- @attributes = @attributes.map(&:forgetting_assignment)
- end
-
- def clear_mutation_trackers
- @mutation_tracker = nil
- @mutations_from_database = nil
- end
-
- def mutations_before_last_save
- @mutations_before_last_save ||= NullMutationTracker.instance
- end
-
- def cache_changed_attributes
- @cached_changed_attributes = changed_attributes
- yield
- ensure
- clear_changed_attributes_cache
- end
-
- def clear_changed_attributes_cache
- remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes)
- end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb
index 63c059e291..9b267bb7c0 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -17,13 +17,15 @@ module ActiveRecord
# Returns the primary key value.
def id
sync_with_transaction_state
- _read_attribute(self.class.primary_key) if self.class.primary_key
+ primary_key = self.class.primary_key
+ _read_attribute(primary_key) if primary_key
end
# Sets the primary key value.
def id=(value)
sync_with_transaction_state
- _write_attribute(self.class.primary_key, value) if self.class.primary_key
+ primary_key = self.class.primary_key
+ _write_attribute(primary_key, value) if primary_key
end
# Queries the primary key value.
@@ -81,7 +83,7 @@ module ActiveRecord
end
def reset_primary_key #:nodoc:
- if self == base_class
+ if base_class?
self.primary_key = get_primary_key(base_class.name)
else
self.primary_key = base_class.primary_key
@@ -129,7 +131,7 @@ module ActiveRecord
def suppress_composite_primary_key(pk)
return pk unless pk.is_a?(Array)
- warn <<-WARNING.strip_heredoc
+ warn <<~WARNING
WARNING: Active Record does not support composite primary key.
#{table_name} has composite primary key. Composite primary key is ignored.
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index b070235684..0f7bcba564 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -27,7 +27,7 @@ module ActiveRecord
# Making it frozen means that it doesn't get duped when used to
# key the @attributes in read_attribute.
def define_method_attribute(name)
- safe_name = name.unpack("h*".freeze).first
+ safe_name = name.unpack1("h*".freeze)
temp_method = "__temp__#{safe_name}"
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
@@ -58,8 +58,9 @@ module ActiveRecord
attr_name.to_s
end
- name = self.class.primary_key if name == "id".freeze && self.class.primary_key
- sync_with_transaction_state if name == self.class.primary_key
+ primary_key = self.class.primary_key
+ name = primary_key if name == "id".freeze && primary_key
+ sync_with_transaction_state if name == primary_key
_read_attribute(name, &block)
end
@@ -68,7 +69,7 @@ module ActiveRecord
if defined?(JRUBY_VERSION)
# This form is significantly faster on JRuby, and this is one of our biggest hotspots.
# https://github.com/jruby/jruby/pull/2562
- def _read_attribute(attr_name, &block) # :nodoc
+ def _read_attribute(attr_name, &block) # :nodoc:
@attributes.fetch_value(attr_name.to_s, &block)
end
else
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index ebc2baed34..6e0e90f39c 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -7,7 +7,7 @@ module ActiveRecord
class ColumnNotSerializableError < StandardError
def initialize(name, type)
- super <<-EOS.strip_heredoc
+ super <<~EOS
Column `#{name}` of type #{type.class} does not support `serialize` feature.
Usually it means that you are trying to use `serialize`
on a column that already implements serialization natively.
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index 37891ce2ef..c7521422bb 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -13,7 +13,7 @@ module ActiveRecord
private
def define_method_attribute=(name)
- safe_name = name.unpack("h*".freeze).first
+ safe_name = name.unpack1("h*".freeze)
ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key
@@ -39,8 +39,9 @@ module ActiveRecord
attr_name.to_s
end
- name = self.class.primary_key if name == "id".freeze && self.class.primary_key
- sync_with_transaction_state if name == self.class.primary_key
+ primary_key = self.class.primary_key
+ name = primary_key if name == "id".freeze && primary_key
+ sync_with_transaction_state if name == primary_key
_write_attribute(name, value)
end
diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb
deleted file mode 100644
index 94bf641a5d..0000000000
--- a/activerecord/lib/active_record/attribute_mutation_tracker.rb
+++ /dev/null
@@ -1,111 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- class AttributeMutationTracker # :nodoc:
- OPTION_NOT_GIVEN = Object.new
-
- def initialize(attributes)
- @attributes = attributes
- @forced_changes = Set.new
- end
-
- def changed_values
- attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
- if changed?(attr_name)
- result[attr_name] = attributes[attr_name].original_value
- end
- end
- end
-
- def changes
- attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
- change = change_to_attribute(attr_name)
- if change
- result[attr_name] = change
- end
- end
- end
-
- def change_to_attribute(attr_name)
- attr_name = attr_name.to_s
- if changed?(attr_name)
- [attributes[attr_name].original_value, attributes.fetch_value(attr_name)]
- end
- end
-
- def any_changes?
- attr_names.any? { |attr| changed?(attr) }
- end
-
- def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
- attr_name = attr_name.to_s
- forced_changes.include?(attr_name) ||
- attributes[attr_name].changed? &&
- (OPTION_NOT_GIVEN == from || attributes[attr_name].original_value == from) &&
- (OPTION_NOT_GIVEN == to || attributes[attr_name].value == to)
- end
-
- def changed_in_place?(attr_name)
- attributes[attr_name.to_s].changed_in_place?
- end
-
- def forget_change(attr_name)
- attr_name = attr_name.to_s
- attributes[attr_name] = attributes[attr_name].forgetting_assignment
- forced_changes.delete(attr_name)
- end
-
- def original_value(attr_name)
- attributes[attr_name.to_s].original_value
- end
-
- def force_change(attr_name)
- forced_changes << attr_name.to_s
- end
-
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :attributes, :forced_changes
-
- private
-
- def attr_names
- attributes.keys
- end
- end
-
- class NullMutationTracker # :nodoc:
- include Singleton
-
- def changed_values(*)
- {}
- end
-
- def changes(*)
- {}
- end
-
- def change_to_attribute(attr_name)
- end
-
- def any_changes?(*)
- false
- end
-
- def changed?(*)
- false
- end
-
- def changed_in_place?(*)
- false
- end
-
- def forget_change(*)
- end
-
- def original_value(*)
- end
- end
-end
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb
deleted file mode 100644
index 492067e2b3..0000000000
--- a/activerecord/lib/active_record/attribute_set.rb
+++ /dev/null
@@ -1,113 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "attribute_set/builder"
-require_relative "attribute_set/yaml_encoder"
-
-module ActiveRecord
- class AttributeSet # :nodoc:
- delegate :each_value, :fetch, to: :attributes
-
- def initialize(attributes)
- @attributes = attributes
- end
-
- def [](name)
- attributes[name] || Attribute.null(name)
- end
-
- def []=(name, value)
- attributes[name] = value
- end
-
- def values_before_type_cast
- attributes.transform_values(&:value_before_type_cast)
- end
-
- def to_hash
- initialized_attributes.transform_values(&:value)
- end
- alias_method :to_h, :to_hash
-
- def key?(name)
- attributes.key?(name) && self[name].initialized?
- end
-
- def keys
- attributes.each_key.select { |name| self[name].initialized? }
- end
-
- if defined?(JRUBY_VERSION)
- # This form is significantly faster on JRuby, and this is one of our biggest hotspots.
- # https://github.com/jruby/jruby/pull/2562
- def fetch_value(name, &block)
- self[name].value(&block)
- end
- else
- def fetch_value(name)
- self[name].value { |n| yield n if block_given? }
- end
- end
-
- def write_from_database(name, value)
- attributes[name] = self[name].with_value_from_database(value)
- end
-
- def write_from_user(name, value)
- attributes[name] = self[name].with_value_from_user(value)
- end
-
- def write_cast_value(name, value)
- attributes[name] = self[name].with_cast_value(value)
- end
-
- def freeze
- @attributes.freeze
- super
- end
-
- def deep_dup
- self.class.allocate.tap do |copy|
- copy.instance_variable_set(:@attributes, attributes.deep_dup)
- end
- end
-
- def initialize_dup(_)
- @attributes = attributes.dup
- super
- end
-
- def initialize_clone(_)
- @attributes = attributes.clone
- super
- end
-
- def reset(key)
- if key?(key)
- write_from_database(key, nil)
- end
- end
-
- def accessed
- attributes.select { |_, attr| attr.has_been_read? }.keys
- end
-
- def map(&block)
- new_attributes = attributes.transform_values(&block)
- AttributeSet.new(new_attributes)
- end
-
- def ==(other)
- attributes == other.attributes
- end
-
- protected
-
- attr_reader :attributes
-
- private
-
- def initialized_attributes
- attributes.select { |_, attr| attr.initialized? }
- end
- end
-end
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
deleted file mode 100644
index e3a9c7fdb3..0000000000
--- a/activerecord/lib/active_record/attribute_set/builder.rb
+++ /dev/null
@@ -1,126 +0,0 @@
-# frozen_string_literal: true
-
-require_relative "../attribute"
-
-module ActiveRecord
- class AttributeSet # :nodoc:
- class Builder # :nodoc:
- attr_reader :types, :always_initialized, :default
-
- def initialize(types, always_initialized = nil, &default)
- @types = types
- @always_initialized = always_initialized
- @default = default
- end
-
- def build_from_database(values = {}, additional_types = {})
- if always_initialized && !values.key?(always_initialized)
- values[always_initialized] = nil
- end
-
- attributes = LazyAttributeHash.new(types, values, additional_types, &default)
- AttributeSet.new(attributes)
- end
- end
- end
-
- class LazyAttributeHash # :nodoc:
- delegate :transform_values, :each_key, :each_value, :fetch, to: :materialize
-
- def initialize(types, values, additional_types, &default)
- @types = types
- @values = values
- @additional_types = additional_types
- @materialized = false
- @delegate_hash = {}
- @default = default || proc {}
- end
-
- def key?(key)
- delegate_hash.key?(key) || values.key?(key) || types.key?(key)
- end
-
- def [](key)
- delegate_hash[key] || assign_default_value(key)
- end
-
- def []=(key, value)
- if frozen?
- raise RuntimeError, "Can't modify frozen hash"
- end
- delegate_hash[key] = value
- end
-
- def deep_dup
- dup.tap do |copy|
- copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
- end
- end
-
- def initialize_dup(_)
- @delegate_hash = Hash[delegate_hash]
- super
- end
-
- def select
- keys = types.keys | values.keys | delegate_hash.keys
- keys.each_with_object({}) do |key, hash|
- attribute = self[key]
- if yield(key, attribute)
- hash[key] = attribute
- end
- end
- end
-
- def ==(other)
- if other.is_a?(LazyAttributeHash)
- materialize == other.materialize
- else
- materialize == other
- end
- end
-
- def marshal_dump
- materialize
- end
-
- def marshal_load(delegate_hash)
- @delegate_hash = delegate_hash
- @types = {}
- @values = {}
- @additional_types = {}
- @materialized = true
- end
-
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :types, :values, :additional_types, :delegate_hash, :default
-
- def materialize
- unless @materialized
- values.each_key { |key| self[key] }
- types.each_key { |key| self[key] }
- unless frozen?
- @materialized = true
- end
- end
- delegate_hash
- end
-
- private
-
- def assign_default_value(name)
- type = additional_types.fetch(name, types[name])
- value_present = true
- value = values.fetch(name) { value_present = false }
-
- if value_present
- delegate_hash[name] = Attribute.from_database(name, value, type)
- elsif types.key?(name)
- delegate_hash[name] = default.call(name) || Attribute.uninitialized(name, type)
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb
deleted file mode 100644
index 9254ce16ab..0000000000
--- a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-module ActiveRecord
- class AttributeSet
- # Attempts to do more intelligent YAML dumping of an
- # ActiveRecord::AttributeSet to reduce the size of the resulting string
- class YAMLEncoder # :nodoc:
- def initialize(default_types)
- @default_types = default_types
- end
-
- def encode(attribute_set, coder)
- coder["concise_attributes"] = attribute_set.each_value.map do |attr|
- if attr.type.equal?(default_types[attr.name])
- attr.with_type(nil)
- else
- attr
- end
- end
- end
-
- def decode(coder)
- if coder["attributes"]
- coder["attributes"]
- else
- attributes_hash = Hash[coder["concise_attributes"].map do |attr|
- if attr.type.nil?
- attr = attr.with_type(default_types[attr.name])
- end
- [attr.name, attr]
- end]
- AttributeSet.new(attributes_hash)
- end
- end
-
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :default_types
- end
- end
-end
diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb
index afb559db71..35150889d9 100644
--- a/activerecord/lib/active_record/attributes.rb
+++ b/activerecord/lib/active_record/attributes.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "attribute/user_provided_default"
+require "active_model/attribute/user_provided_default"
module ActiveRecord
# See ActiveRecord::Attributes::ClassMethods for documentation
@@ -57,7 +57,7 @@ module ActiveRecord
# store_listing = StoreListing.new(price_in_cents: '10.1')
#
# # before
- # store_listing.price_in_cents # => BigDecimal.new(10.1)
+ # store_listing.price_in_cents # => BigDecimal(10.1)
#
# class StoreListing < ActiveRecord::Base
# attribute :price_in_cents, :integer
@@ -250,14 +250,14 @@ module ActiveRecord
if value == NO_DEFAULT_PROVIDED
default_attribute = _default_attributes[name].with_type(type)
elsif from_user
- default_attribute = Attribute::UserProvidedDefault.new(
+ default_attribute = ActiveModel::Attribute::UserProvidedDefault.new(
name,
value,
type,
_default_attributes.fetch(name.to_s) { nil },
)
else
- default_attribute = Attribute.from_database(name, value, type)
+ default_attribute = ActiveModel::Attribute.from_database(name, value, type)
end
_default_attributes[name] = default_attribute
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index 6974cf74f6..a1250c3835 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -436,6 +436,9 @@ module ActiveRecord
if (autosave && record.changed_for_autosave?) || new_record? || record_changed?(reflection, record, key)
unless reflection.through_reflection
record[reflection.foreign_key] = key
+ if inverse_reflection = reflection.inverse_of
+ record.association(inverse_reflection.name).loaded!
+ end
end
saved = record.save(validate: !autosave)
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index 541ff51fbe..7ab9160265 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -9,20 +9,19 @@ require "active_support/core_ext/module/attribute_accessors"
require "active_support/core_ext/array/extract_options"
require "active_support/core_ext/hash/deep_merge"
require "active_support/core_ext/hash/slice"
-require "active_support/core_ext/hash/transform_values"
require "active_support/core_ext/string/behavior"
require "active_support/core_ext/kernel/singleton_class"
require "active_support/core_ext/module/introspection"
require "active_support/core_ext/object/duplicable"
require "active_support/core_ext/class/subclasses"
-require_relative "attribute_decorators"
-require_relative "define_callbacks"
-require_relative "errors"
-require_relative "log_subscriber"
-require_relative "explain_subscriber"
-require_relative "relation/delegation"
-require_relative "attributes"
-require_relative "type_caster"
+require "active_record/attribute_decorators"
+require "active_record/define_callbacks"
+require "active_record/errors"
+require "active_record/log_subscriber"
+require "active_record/explain_subscriber"
+require "active_record/relation/delegation"
+require "active_record/attributes"
+require "active_record/type_caster"
module ActiveRecord #:nodoc:
# = Active Record
@@ -291,6 +290,7 @@ module ActiveRecord #:nodoc:
extend CollectionCacheKey
include Core
+ include DatabaseConfigurations
include Persistence
include ReadonlyAttributes
include ModelSchema
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index a2439e6ec7..fd6819d08f 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -75,21 +75,7 @@ module ActiveRecord
# end
#
# Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is
- # run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation
- # where the +before_destroy+ method is overridden:
- #
- # class Topic < ActiveRecord::Base
- # def before_destroy() destroy_author end
- # end
- #
- # class Reply < Topic
- # def before_destroy() destroy_readers end
- # end
- #
- # In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+.
- # So, use the callback macros when you want to ensure that a certain callback is called for the entire
- # hierarchy, and use the regular overwritable methods when you want to leave it up to each descendant
- # to decide whether they want to call +super+ and trigger the inherited callbacks.
+ # run, both +destroy_author+ and +destroy_readers+ are called.
#
# *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the
# callbacks before specifying the associations. Otherwise, you might trigger the loading of a
@@ -98,9 +84,9 @@ module ActiveRecord
# == Types of callbacks
#
# There are four types of callbacks accepted by the callback macros: Method references (symbol), callback objects,
- # inline methods (using a proc), and inline eval methods (using a string). Method references and callback objects
+ # inline methods (using a proc). Method references and callback objects
# are the recommended approaches, inline methods using a proc are sometimes appropriate (such as for
- # creating mix-ins), and inline eval methods are deprecated.
+ # creating mix-ins).
#
# The method reference callbacks work by specifying a protected or private method available in the object, like this:
#
@@ -232,7 +218,7 @@ module ActiveRecord
#
# For example:
#
- # class Topic
+ # class Topic < ActiveRecord::Base
# has_many :children
#
# after_save :log_children
@@ -240,7 +226,7 @@ module ActiveRecord
#
# private
#
- # def log_chidren
+ # def log_children
# # Child processing
# end
#
@@ -257,7 +243,7 @@ module ActiveRecord
#
# For example:
#
- # class Topic
+ # class Topic < ActiveRecord::Base
# has_many :children
#
# after_commit :log_children
@@ -265,7 +251,7 @@ module ActiveRecord
#
# private
#
- # def log_chidren
+ # def log_children
# # Child processing
# end
#
diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb
index f3e6516414..dfba78614e 100644
--- a/activerecord/lib/active_record/collection_cache_key.rb
+++ b/activerecord/lib/active_record/collection_cache_key.rb
@@ -3,22 +3,24 @@
module ActiveRecord
module CollectionCacheKey
def collection_cache_key(collection = all, timestamp_column = :updated_at) # :nodoc:
- query_signature = Digest::MD5.hexdigest(collection.to_sql)
+ query_signature = ActiveSupport::Digest.hexdigest(collection.to_sql)
key = "#{collection.model_name.cache_key}/query-#{query_signature}"
- if collection.loaded?
- size = collection.size
+ if collection.loaded? || collection.distinct_value
+ size = collection.records.size
if size > 0
timestamp = collection.max_by(&timestamp_column)._read_attribute(timestamp_column)
end
else
- column_type = type_for_attribute(timestamp_column.to_s)
+ if collection.eager_loading?
+ collection = collection.send(:apply_join_dependency)
+ end
+ column_type = type_for_attribute(timestamp_column)
column = connection.column_name_from_arel_node(collection.arel_attribute(timestamp_column))
select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp"
if collection.has_limit_or_offset?
- query = collection.spawn
- query.select_values = [column]
+ query = collection.select(column)
subquery_alias = "subquery_for_cache_key"
subquery_column = "#{subquery_alias}.#{timestamp_column}"
subquery = query.arel.as(subquery_alias)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
index 0759f4d2b3..c730584902 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -63,15 +63,13 @@ module ActiveRecord
# There are several connection-pooling-related options that you can add to
# your database connection configuration:
#
- # * +pool+: number indicating size of connection pool (default 5)
- # * +checkout_timeout+: number of seconds to block and wait for a connection
- # before giving up and raising a timeout error (default 5 seconds).
- # * +reaping_frequency+: frequency in seconds to periodically run the
- # Reaper, which attempts to find and recover connections from dead
- # threads, which can occur if a programmer forgets to close a
- # connection at the end of a thread or a thread dies unexpectedly.
- # Regardless of this setting, the Reaper will be invoked before every
- # blocking wait. (Default +nil+, which means don't schedule the Reaper).
+ # * +pool+: maximum number of connections the pool may manage (default 5).
+ # * +idle_timeout+: number of seconds that a connection will be kept
+ # unused in the pool before it is automatically disconnected (default
+ # 300 seconds). Set this to zero to keep connections forever.
+ # * +checkout_timeout+: number of seconds to wait for a connection to
+ # become available before giving up and raising a timeout error (default
+ # 5 seconds).
#
#--
# Synchronization policy:
@@ -82,11 +80,8 @@ module ActiveRecord
# * private methods that require being called in a +synchronize+ blocks
# are now explicitly documented
class ConnectionPool
- # Threadsafe, fair, FIFO queue. Meant to be used by ConnectionPool
- # with which it shares a Monitor. But could be a generic Queue.
- #
- # The Queue in stdlib's 'thread' could replace this class except
- # stdlib's doesn't support waiting with a timeout.
+ # Threadsafe, fair, LIFO queue. Meant to be used by ConnectionPool
+ # with which it shares a Monitor.
class Queue
def initialize(lock = Monitor.new)
@lock = lock
@@ -175,7 +170,7 @@ module ActiveRecord
# Removes and returns the head of the queue if possible, or +nil+.
def remove
- @queue.shift
+ @queue.pop
end
# Remove and return the head the queue if the number of
@@ -283,12 +278,12 @@ module ActiveRecord
end
end
- # Every +frequency+ seconds, the reaper will call +reap+ on +pool+.
- # A reaper instantiated with a +nil+ frequency will never reap the
- # connection pool.
+ # Every +frequency+ seconds, the reaper will call +reap+ and +flush+ on
+ # +pool+. A reaper instantiated with a zero frequency will never reap
+ # the connection pool.
#
- # Configure the frequency by setting "reaping_frequency" in your
- # database yaml file.
+ # Configure the frequency by setting +reaping_frequency+ in your database
+ # yaml file (default 60 seconds).
class Reaper
attr_reader :pool, :frequency
@@ -298,11 +293,12 @@ module ActiveRecord
end
def run
- return unless frequency
+ return unless frequency && frequency > 0
Thread.new(frequency, pool) { |t, p|
loop do
sleep t
p.reap
+ p.flush
end
}
end
@@ -326,6 +322,10 @@ module ActiveRecord
@spec = spec
@checkout_timeout = (spec.config[:checkout_timeout] && spec.config[:checkout_timeout].to_f) || 5
+ if @idle_timeout = spec.config.fetch(:idle_timeout, 300)
+ @idle_timeout = @idle_timeout.to_f
+ @idle_timeout = nil if @idle_timeout <= 0
+ end
# default max pool size to 5
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
@@ -356,7 +356,10 @@ module ActiveRecord
@lock_thread = false
- @reaper = Reaper.new(self, spec.config[:reaping_frequency] && spec.config[:reaping_frequency].to_f)
+ # +reaping_frequency+ is configurable mostly for historical reasons, but it could
+ # also be useful if someone wants a very low +idle_timeout+.
+ reaping_frequency = spec.config.fetch(:reaping_frequency, 60)
+ @reaper = Reaper.new(self, reaping_frequency && reaping_frequency.to_f)
@reaper.run
end
@@ -450,6 +453,21 @@ module ActiveRecord
disconnect(false)
end
+ # Discards all connections in the pool (even if they're currently
+ # leased!), along with the pool itself. Any further interaction with the
+ # pool (except #spec and #schema_cache) is undefined.
+ #
+ # See AbstractAdapter#discard!
+ def discard! # :nodoc:
+ synchronize do
+ return if @connections.nil? # already discarded
+ @connections.each do |conn|
+ conn.discard!
+ end
+ @connections = @available = @thread_cached_conns = nil
+ end
+ end
+
# Clears the cache which maps classes and re-connects connections that
# require reloading.
#
@@ -575,6 +593,35 @@ module ActiveRecord
end
end
+ # Disconnect all connections that have been idle for at least
+ # +minimum_idle+ seconds. Connections currently checked out, or that were
+ # checked in less than +minimum_idle+ seconds ago, are unaffected.
+ def flush(minimum_idle = @idle_timeout)
+ return if minimum_idle.nil?
+
+ idle_connections = synchronize do
+ @connections.select do |conn|
+ !conn.in_use? && conn.seconds_idle >= minimum_idle
+ end.each do |conn|
+ conn.lease
+
+ @available.delete conn
+ @connections.delete conn
+ end
+ end
+
+ idle_connections.each do |conn|
+ conn.disconnect!
+ end
+ end
+
+ # Disconnect all currently idle connections. Connections currently checked
+ # out are unaffected.
+ def flush!
+ reap
+ flush(-1)
+ end
+
def num_waiting_in_queue # :nodoc:
@available.num_waiting
end
@@ -866,11 +913,31 @@ module ActiveRecord
# about the model. The model needs to pass a specification name to the handler,
# in order to look up the correct connection pool.
class ConnectionHandler
+ def self.unowned_pool_finalizer(pid_map) # :nodoc:
+ lambda do |_|
+ discard_unowned_pools(pid_map)
+ end
+ end
+
+ def self.discard_unowned_pools(pid_map) # :nodoc:
+ pid_map.each do |pid, pools|
+ pools.values.compact.each(&:discard!) unless pid == Process.pid
+ end
+ end
+
def initialize
# These caches are keyed by spec.name (ConnectionSpecification#name).
@owner_to_pool = Concurrent::Map.new(initial_capacity: 2) do |h, k|
+ # Discard the parent's connection pools immediately; we have no need
+ # of them
+ ConnectionHandler.discard_unowned_pools(h)
+
h[k] = Concurrent::Map.new(initial_capacity: 2)
end
+
+ # Backup finalizer: if the forked child never needed a pool, the above
+ # early discard has not occurred
+ ObjectSpace.define_finalizer self, ConnectionHandler.unowned_pool_finalizer(@owner_to_pool)
end
def connection_pool_list
@@ -924,6 +991,13 @@ module ActiveRecord
connection_pool_list.each(&:disconnect!)
end
+ # Disconnects all currently idle connections.
+ #
+ # See ConnectionPool#flush! for details.
+ def flush_idle_connections!
+ connection_pool_list.each(&:flush!)
+ end
+
# Locate the connection of the nearest super class. This can be an
# active or defined connection: if it is the latter, it will be
# opened and set as the active connection for the class it was defined
@@ -931,9 +1005,7 @@ module ActiveRecord
def retrieve_connection(spec_name) #:nodoc:
pool = retrieve_connection_pool(spec_name)
raise ConnectionNotEstablished, "No connection pool with '#{spec_name}' found." unless pool
- conn = pool.connection
- raise ConnectionNotEstablished, "No connection for '#{spec_name}' in connection pool" unless conn
- conn
+ pool.connection
end
# Returns true if a connection that's accessible to this class has
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
index 36048bee03..41553cfa83 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -356,38 +356,36 @@ module ActiveRecord
# Inserts a set of fixtures into the table. Overridden in adapters that require
# something beyond a simple insert (eg. Oracle).
def insert_fixtures(fixtures, table_name)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `insert_fixtures` is deprecated and will be removed in the next version of Rails.
+ Consider using `insert_fixtures_set` for performance improvement.
+ MSG
return if fixtures.empty?
- columns = schema_cache.columns_hash(table_name)
+ execute(build_fixture_sql(fixtures, table_name), "Fixtures Insert")
+ end
- values = fixtures.map do |fixture|
- fixture = fixture.stringify_keys
+ def insert_fixtures_set(fixture_set, tables_to_delete = [])
+ fixture_inserts = fixture_set.map do |table_name, fixtures|
+ next if fixtures.empty?
- unknown_columns = fixture.keys - columns.keys
- if unknown_columns.any?
- raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.)
- end
+ build_fixture_sql(fixtures, table_name)
+ end.compact
- columns.map do |name, column|
- if fixture.key?(name)
- type = lookup_cast_type_from_column(column)
- bind = Relation::QueryAttribute.new(name, fixture[name], type)
- with_yaml_fallback(bind.value_for_database)
- else
- Arel.sql("DEFAULT")
+ table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name table}".dup }
+ total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts))
+
+ disable_referential_integrity do
+ transaction(requires_new: true) do
+ total_sql.each do |sql|
+ execute sql, "Fixtures Load"
+ yield if block_given?
end
end
end
-
- table = Arel::Table.new(table_name)
- manager = Arel::InsertManager.new
- manager.into(table)
- columns.each_key { |column| manager.columns << table[column] }
- manager.values = manager.create_values_list(values)
- execute manager.to_sql, "Fixtures Insert"
end
- def empty_insert_statement_value
+ def empty_insert_statement_value(primary_key = nil)
"DEFAULT VALUES"
end
@@ -416,6 +414,44 @@ module ActiveRecord
alias join_to_delete join_to_update
private
+ def default_insert_value(column)
+ Arel.sql("DEFAULT")
+ end
+
+ def build_fixture_sql(fixtures, table_name)
+ columns = schema_cache.columns_hash(table_name)
+
+ values = fixtures.map do |fixture|
+ fixture = fixture.stringify_keys
+
+ unknown_columns = fixture.keys - columns.keys
+ if unknown_columns.any?
+ raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.)
+ end
+
+ columns.map do |name, column|
+ if fixture.key?(name)
+ type = lookup_cast_type_from_column(column)
+ bind = Relation::QueryAttribute.new(name, fixture[name], type)
+ with_yaml_fallback(bind.value_for_database)
+ else
+ default_insert_value(column)
+ end
+ end
+ end
+
+ table = Arel::Table.new(table_name)
+ manager = Arel::InsertManager.new
+ manager.into(table)
+ columns.each_key { |column| manager.columns << table[column] }
+ manager.values = manager.create_values_list(values)
+
+ manager.to_sql
+ end
+
+ def combine_multi_statements(total_sql)
+ total_sql.join(";\n")
+ end
# Returns a subquery for the given key using the join information.
def subquery_for(key, select)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 9ad04c3216..aec5fa6ba1 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -11,19 +11,6 @@ module ActiveRecord
def quote(value)
value = id_value_for_database(value) if value.is_a?(Base)
- if value.respond_to?(:quoted_id)
- at = value.method(:quoted_id).source_location
- at &&= " at %s:%d" % at
-
- owner = value.method(:quoted_id).owner.to_s
- klass = value.class.to_s
- klass += "(#{owner})" unless owner == klass
-
- ActiveSupport::Deprecation.warn \
- "Defining #quoted_id is deprecated and will be ignored in Rails 5.2. (defined on #{klass}#{at})"
- return value.quoted_id
- end
-
if value.respond_to?(:value_for_database)
value = value.value_for_database
end
@@ -37,10 +24,6 @@ module ActiveRecord
def type_cast(value, column = nil)
value = id_value_for_database(value) if value.is_a?(Base)
- if value.respond_to?(:quoted_id) && value.respond_to?(:id)
- return value.id
- end
-
if column
value = type_cast_from_column(column, value)
end
@@ -147,7 +130,7 @@ module ActiveRecord
end
def quoted_time(value) # :nodoc:
- quoted_date(value).sub(/\A2000-01-01 /, "")
+ quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "")
end
def quoted_binary(value) # :nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
index bd05fb8f6e..529c9d8ca6 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/core_ext/string/strip"
-
module ActiveRecord
module ConnectionAdapters
class AbstractAdapter
@@ -17,9 +15,8 @@ module ActiveRecord
end
delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql,
- :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options, to: :@conn
- private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql,
- :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options
+ :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options,
+ to: :@conn, private: true
private
@@ -66,7 +63,7 @@ module ActiveRecord
end
def visit_ForeignKeyDefinition(o)
- sql = <<-SQL.strip_heredoc
+ sql = +<<~SQL
CONSTRAINT #{quote_column_name(o.name)}
FOREIGN KEY (#{quote_column_name(o.column)})
REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)})
@@ -95,6 +92,7 @@ module ActiveRecord
if options_sql = options[:options]
create_sql << " #{options_sql}"
end
+ create_sql
end
def column_options(o)
@@ -132,7 +130,7 @@ module ActiveRecord
when :cascade then "ON #{action} CASCADE"
when :restrict then "ON #{action} RESTRICT"
else
- raise ArgumentError, <<-MSG.strip_heredoc
+ raise ArgumentError, <<~MSG
'#{dependency}' is not supported for :on_update or :on_delete.
Supported values are: :nullify, :cascade, :restrict
MSG
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index 788a455773..6a498b353c 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -6,7 +6,7 @@ module ActiveRecord
# this type are typically created and returned by methods in database
# adapters. e.g. ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#indexes
class IndexDefinition # :nodoc:
- attr_reader :table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment
+ attr_reader :table, :name, :unique, :columns, :lengths, :orders, :opclasses, :where, :type, :using, :comment
def initialize(
table, name,
@@ -14,6 +14,7 @@ module ActiveRecord
columns = [],
lengths: {},
orders: {},
+ opclasses: {},
where: nil,
type: nil,
using: nil,
@@ -23,13 +24,23 @@ module ActiveRecord
@name = name
@unique = unique
@columns = columns
- @lengths = lengths
- @orders = orders
+ @lengths = concise_options(lengths)
+ @orders = concise_options(orders)
+ @opclasses = concise_options(opclasses)
@where = where
@type = type
@using = using
@comment = comment
end
+
+ private
+ def concise_options(options)
+ if columns.size == options.size && options.values.uniq.size == 1
+ options.values.first
+ else
+ options
+ end
+ end
end
# Abstract representation of a column definition. Instances of this type
@@ -85,6 +96,15 @@ module ActiveRecord
options[:primary_key] != default_primary_key
end
+ def validate?
+ options.fetch(:validate, true)
+ end
+ alias validated? validate?
+
+ def export_name_on_schema_dump?
+ name !~ ActiveRecord::SchemaDumper.fk_ignore_pattern
+ end
+
def defined_for?(to_table_ord = nil, to_table: nil, **options)
if to_table_ord
self.to_table == to_table_ord.to_s
@@ -135,13 +155,8 @@ module ActiveRecord
end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options
-
private
+ attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options
def as_options(value)
value.is_a?(Hash) ? value : {}
@@ -204,6 +219,7 @@ module ActiveRecord
:decimal,
:float,
:integer,
+ :json,
:string,
:text,
:time,
@@ -396,6 +412,9 @@ module ActiveRecord
alias :belongs_to :references
def new_column_definition(name, type, **options) # :nodoc:
+ if integer_like_primary_key?(type, options)
+ type = integer_like_primary_key_type(type, options)
+ end
type = aliased_types(type.to_s, type)
options[:primary_key] ||= type == :primary_key
options[:null] = false if options[:primary_key]
@@ -410,6 +429,14 @@ module ActiveRecord
def aliased_types(name, fallback)
"timestamp" == name ? :datetime : fallback
end
+
+ def integer_like_primary_key?(type, options)
+ options[:primary_key] && [:integer, :bigint].include?(type) && !options.key?(:default)
+ end
+
+ def integer_like_primary_key_type(type, options)
+ type
+ end
end
class AlterTable # :nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
index 1926603474..622e00fffb 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/core_ext/hash/compact"
-
module ActiveRecord
module ConnectionAdapters # :nodoc:
class SchemaDumper < SchemaDumper # :nodoc:
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index f57c7a5d4d..ac73337aef 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-require_relative "../../migration/join_table"
+require "active_record/migration/join_table"
require "active_support/core_ext/string/access"
-require "digest"
+require "digest/sha2"
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -79,7 +79,7 @@ module ActiveRecord
end
# Returns an array of indexes for the given table.
- def indexes(table_name, name = nil)
+ def indexes(table_name)
raise NotImplementedError, "#indexes is not implemented"
end
@@ -216,7 +216,7 @@ module ActiveRecord
# generates:
#
# CREATE TABLE suppliers (
- # id int auto_increment PRIMARY KEY
+ # id bigint auto_increment PRIMARY KEY
# ) ENGINE=InnoDB DEFAULT CHARSET=utf8
#
# ====== Rename the primary key column
@@ -228,7 +228,7 @@ module ActiveRecord
# generates:
#
# CREATE TABLE objects (
- # guid int auto_increment PRIMARY KEY,
+ # guid bigint auto_increment PRIMARY KEY,
# name varchar(80)
# )
#
@@ -255,8 +255,8 @@ module ActiveRecord
# generates:
#
# CREATE TABLE order (
- # product_id integer NOT NULL,
- # client_id integer NOT NULL
+ # product_id bigint NOT NULL,
+ # client_id bigint NOT NULL
# );
#
# ALTER TABLE ONLY "orders"
@@ -265,15 +265,15 @@ module ActiveRecord
# ====== Do not add a primary key column
#
# create_table(:categories_suppliers, id: false) do |t|
- # t.column :category_id, :integer
- # t.column :supplier_id, :integer
+ # t.column :category_id, :bigint
+ # t.column :supplier_id, :bigint
# end
#
# generates:
#
# CREATE TABLE categories_suppliers (
- # category_id int,
- # supplier_id int
+ # category_id bigint,
+ # supplier_id bigint
# )
#
# ====== Create a temporary table based on a query
@@ -305,7 +305,8 @@ module ActiveRecord
yield td if block_given?
if options[:force]
- drop_table(table_name, **options, if_exists: true)
+ drop_opts = { if_exists: true }.merge(**options)
+ drop_table(table_name, drop_opts)
end
result = execute schema_creation.accept td
@@ -361,8 +362,8 @@ module ActiveRecord
# generates:
#
# CREATE TABLE assemblies_parts (
- # assembly_id int NOT NULL,
- # part_id int NOT NULL,
+ # assembly_id bigint NOT NULL,
+ # part_id bigint NOT NULL,
# ) ENGINE=InnoDB DEFAULT CHARSET=utf8
#
def create_join_table(table_1, table_2, column_options: {}, **options)
@@ -406,7 +407,7 @@ module ActiveRecord
#
# Defaults to false.
#
- # Only supported on the MySQL adapter, ignored elsewhere.
+ # Only supported on the MySQL and PostgreSQL adapter, ignored elsewhere.
#
# ====== Add a column
#
@@ -432,7 +433,7 @@ module ActiveRecord
# t.references :company
# end
#
- # Creates a <tt>company_id(integer)</tt> column.
+ # Creates a <tt>company_id(bigint)</tt> column.
#
# ====== Add a polymorphic foreign key column
#
@@ -440,7 +441,7 @@ module ActiveRecord
# t.belongs_to :company, polymorphic: true
# end
#
- # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns.
+ # Creates <tt>company_type(varchar)</tt> and <tt>company_id(bigint)</tt> columns.
#
# ====== Remove a column
#
@@ -522,6 +523,8 @@ module ActiveRecord
# Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns.
# * <tt>:scale</tt> -
# Specifies the scale for the <tt>:decimal</tt> and <tt>:numeric</tt> columns.
+ # * <tt>:comment</tt> -
+ # Specifies the comment for the column. This option is ignored by some backends.
#
# Note: The precision is the total number of significant digits,
# and the scale is the number of digits that can be stored following
@@ -598,7 +601,7 @@ module ActiveRecord
# to provide these in a migration's +change+ method so it can be reverted.
# In that case, +type+ and +options+ will be used by #add_column.
def remove_column(table_name, column_name, type = nil, options = {})
- execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{remove_column_for_alter(table_name, column_name, type, options)}"
end
# Changes the column's definition according to the new options.
@@ -713,7 +716,7 @@ module ActiveRecord
#
# CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname)
#
- # Note: MySQL doesn't yet support index order (it accepts the syntax but ignores it).
+ # Note: MySQL only supports index order from 8.0.1 onwards (earlier versions accepted the syntax but ignored it).
#
# ====== Creating a partial index
#
@@ -736,6 +739,28 @@ module ActiveRecord
#
# Note: only supported by PostgreSQL and MySQL
#
+ # ====== Creating an index with a specific operator class
+ #
+ # add_index(:developers, :name, using: 'gist', opclass: :gist_trgm_ops)
+ #
+ # generates:
+ #
+ # CREATE INDEX developers_on_name ON developers USING gist (name gist_trgm_ops) -- PostgreSQL
+ #
+ # add_index(:developers, [:name, :city], using: 'gist', opclass: { city: :gist_trgm_ops })
+ #
+ # generates:
+ #
+ # CREATE INDEX developers_on_name_and_city ON developers USING gist (name, city gist_trgm_ops) -- PostgreSQL
+ #
+ # add_index(:developers, [:name, :city], using: 'gist', opclass: :gist_trgm_ops)
+ #
+ # generates:
+ #
+ # CREATE INDEX developers_on_name_and_city ON developers USING gist (name gist_trgm_ops, city gist_trgm_ops) -- PostgreSQL
+ #
+ # Note: only supported by PostgreSQL
+ #
# ====== Creating an index with a specific type
#
# add_index(:developers, :name, type: :fulltext)
@@ -804,24 +829,19 @@ module ActiveRecord
end
# Verifies the existence of an index with a given name.
- def index_name_exists?(table_name, index_name, default = nil)
- unless default.nil?
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing default to #index_name_exists? is deprecated without replacement.
- MSG
- end
+ def index_name_exists?(table_name, index_name)
index_name = index_name.to_s
indexes(table_name).detect { |i| i.name == index_name }
end
- # Adds a reference. The reference column is an integer by default,
+ # Adds a reference. The reference column is a bigint by default,
# the <tt>:type</tt> option can be used to specify a different type.
# Optionally adds a +_type+ column, if <tt>:polymorphic</tt> option is provided.
# #add_reference and #add_belongs_to are acceptable.
#
# The +options+ hash can include the following keys:
# [<tt>:type</tt>]
- # The reference column type. Defaults to +:integer+.
+ # The reference column type. Defaults to +:bigint+.
# [<tt>:index</tt>]
# Add an appropriate index. Defaults to true.
# See #add_index for usage of this option.
@@ -832,7 +852,7 @@ module ActiveRecord
# [<tt>:null</tt>]
# Whether the column allows nulls. Defaults to true.
#
- # ====== Create a user_id integer column
+ # ====== Create a user_id bigint column
#
# add_reference(:products, :user)
#
@@ -889,7 +909,7 @@ module ActiveRecord
foreign_key_options = { to_table: reference_name }
end
foreign_key_options[:column] ||= "#{ref_name}_id"
- remove_foreign_key(table_name, **foreign_key_options)
+ remove_foreign_key(table_name, foreign_key_options)
end
remove_column(table_name, "#{ref_name}_id")
@@ -945,6 +965,8 @@ module ActiveRecord
# Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+
# [<tt>:on_update</tt>]
# Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade+ and +:restrict+
+ # [<tt>:validate</tt>]
+ # (Postgres only) Specify whether or not the constraint should be validated. Defaults to +true+.
def add_foreign_key(from_table, to_table, options = {})
return unless supports_foreign_keys?
@@ -1018,16 +1040,6 @@ module ActiveRecord
insert_versions_sql(versions) if versions.any?
end
- def initialize_schema_migrations_table # :nodoc:
- ActiveRecord::SchemaMigration.create_table
- end
- deprecate :initialize_schema_migrations_table
-
- def initialize_internal_metadata_table # :nodoc:
- ActiveRecord::InternalMetadata.create_table
- end
- deprecate :initialize_internal_metadata_table
-
def internal_string_options_for_primary_key # :nodoc:
{ primary_key: true }
end
@@ -1038,8 +1050,8 @@ module ActiveRecord
sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name)
migrated = ActiveRecord::SchemaMigration.all_versions.map(&:to_i)
- versions = ActiveRecord::Migrator.migration_files(migrations_paths).map do |file|
- ActiveRecord::Migrator.parse_migration_filename(file).first.to_i
+ versions = migration_context.migration_files.map do |file|
+ migration_context.parse_migration_filename(file).first.to_i
end
unless migrated.include?(version)
@@ -1133,7 +1145,7 @@ module ActiveRecord
def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc:
column_names = index_column_names(column_name)
- options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type)
+ options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type, :opclass)
index_type = options[:type].to_s if options.key?(:type)
index_type ||= options[:unique] ? "UNIQUE" : ""
@@ -1172,7 +1184,7 @@ module ActiveRecord
end
# Changes the comment for a column or removes it if +nil+.
- def change_column_comment(table_name, column_name, comment) #:nodoc:
+ def change_column_comment(table_name, column_name, comment)
raise NotImplementedError, "#{self.class} does not support changing column comments"
end
@@ -1186,20 +1198,22 @@ module ActiveRecord
end
def add_index_sort_order(quoted_columns, **options)
- if order = options[:order]
- case order
- when Hash
- order = order.symbolize_keys
- quoted_columns.each { |name, column| column << " #{order[name].upcase}" if order[name].present? }
- when String
- quoted_columns.each { |name, column| column << " #{order.upcase}" if order.present? }
- end
+ orders = options_for_index_columns(options[:order])
+ quoted_columns.each do |name, column|
+ column << " #{orders[name].upcase}" if orders[name].present?
end
+ end
- quoted_columns
+ def options_for_index_columns(options)
+ if options.is_a?(Hash)
+ options.symbolize_keys
+ else
+ Hash.new { |hash, column| hash[column] = options }
+ end
end
- # Overridden by the MySQL adapter for supporting index lengths
+ # Overridden by the MySQL adapter for supporting index lengths and by
+ # the PostgreSQL adapter for supporting operator classes.
def add_options_for_index_columns(quoted_columns, **options)
if supports_index_sort_order?
quoted_columns = add_index_sort_order(quoted_columns, options)
@@ -1353,6 +1367,20 @@ module ActiveRecord
options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty?
end
+ def add_column_for_alter(table_name, column_name, type, options = {})
+ td = create_table_definition(table_name)
+ cd = td.new_column_definition(column_name, type, options)
+ schema_creation.accept(AddColumnDefinition.new(cd))
+ end
+
+ def remove_column_for_alter(table_name, column_name, type = nil, options = {})
+ "DROP COLUMN #{quote_column_name(column_name)}"
+ end
+
+ def remove_columns_for_alter(table_name, *column_names)
+ column_names.map { |column_name| remove_column_for_alter(table_name, column_name) }
+ end
+
def insert_versions_sql(versions)
sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 147e16e9fa..0ce3796829 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -75,7 +75,6 @@ module ActiveRecord
class Transaction #:nodoc:
attr_reader :connection, :state, :records, :savepoint_name
- attr_writer :joinable
def initialize(connection, options, run_commit_callbacks: false)
@connection = connection
@@ -240,7 +239,7 @@ module ActiveRecord
rollback_transaction if transaction
else
begin
- commit_transaction
+ commit_transaction if transaction
rescue Exception
rollback_transaction(transaction) unless transaction.state.completed?
raise
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
index 8c889f98f5..559f068c39 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -1,11 +1,11 @@
# frozen_string_literal: true
-require_relative "../type"
-require_relative "determine_if_preparable_visitor"
-require_relative "schema_cache"
-require_relative "sql_type_metadata"
-require_relative "abstract/schema_dumper"
-require_relative "abstract/schema_creation"
+require "active_record/connection_adapters/determine_if_preparable_visitor"
+require "active_record/connection_adapters/schema_cache"
+require "active_record/connection_adapters/sql_type_metadata"
+require "active_record/connection_adapters/abstract/schema_dumper"
+require "active_record/connection_adapters/abstract/schema_creation"
+require "active_support/concurrency/load_interlock_aware_monitor"
require "arel/collectors/bind"
require "arel/collectors/composite"
require "arel/collectors/sql_string"
@@ -105,10 +105,11 @@ module ActiveRecord
@logger = logger
@config = config
@pool = nil
+ @idle_since = Concurrent.monotonic_time
@schema_cache = SchemaCache.new self
@quoted_column_names, @quoted_table_names = {}, {}
@visitor = arel_visitor
- @lock = Monitor.new
+ @lock = ActiveSupport::Concurrency::LoadInterlockAwareMonitor.new
if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
@prepared_statements = true
@@ -118,6 +119,14 @@ module ActiveRecord
end
end
+ def migrations_paths # :nodoc:
+ @config[:migrations_paths] || Migrator.migrations_paths
+ end
+
+ def migration_context # :nodoc:
+ MigrationContext.new(migrations_paths)
+ end
+
class Version
include Comparable
@@ -164,6 +173,7 @@ module ActiveRecord
"Current thread: #{Thread.current}."
end
+ @idle_since = Concurrent.monotonic_time
@owner = nil
else
raise ActiveRecordError, "Cannot expire connection, it is not currently leased."
@@ -183,6 +193,12 @@ module ActiveRecord
end
end
+ # Seconds since this connection was returned to the pool
+ def seconds_idle # :nodoc:
+ return 0 if in_use?
+ Concurrent.monotonic_time - @idle_since
+ end
+
def unprepared_statement
old_prepared_statements, @prepared_statements = @prepared_statements, false
yield
@@ -196,16 +212,6 @@ module ActiveRecord
self.class::ADAPTER_NAME
end
- def supports_migrations? # :nodoc:
- true
- end
- deprecate :supports_migrations?
-
- def supports_primary_key? # :nodoc:
- true
- end
- deprecate :supports_primary_key?
-
# Does this adapter support DDL rollbacks in transactions? That is, would
# CREATE TABLE or ALTER TABLE get rolled back by a transaction?
def supports_ddl_transactions?
@@ -274,6 +280,11 @@ module ActiveRecord
false
end
+ # Does this adapter support creating invalid constraints?
+ def supports_validate_constraints?
+ false
+ end
+
# Does this adapter support creating foreign key constraints
# in the same statement as creating the table?
def supports_foreign_keys_in_create?
@@ -315,6 +326,11 @@ module ActiveRecord
false
end
+ # Does this adapter support foreign/external tables?
+ def supports_foreign_tables?
+ false
+ end
+
# This is meant to be implemented by the adapters that support extensions
def disable_extension(name)
end
@@ -377,6 +393,19 @@ module ActiveRecord
reset_transaction
end
+ # Immediately forget this connection ever existed. Unlike disconnect!,
+ # this will not communicate with the server.
+ #
+ # After calling this method, the behavior of all other methods becomes
+ # undefined. This is called internally just before a forked process gets
+ # rid of a connection that belonged to its parent.
+ def discard!
+ # This should be overridden by concrete adapters.
+ #
+ # Prevent @connection's finalizer from touching the socket, or
+ # otherwise communicating with its server, when it is collected.
+ end
+
# Reset the state of this connection, directing the DBMS to clear
# transactions and other connection-related server-side state. Usually a
# database-dependent operation.
@@ -402,10 +431,7 @@ module ActiveRecord
# Checks whether the connection to the database is still active (i.e. not stale).
# This is done under the hood by calling #active?. If the connection
# is no longer active, then this method will reconnect to the database.
- def verify!(*ignored)
- if ignored.size > 0
- ActiveSupport::Deprecation.warn("Passing arguments to #verify method of the connection has no effect and has been deprecated. Please remove all arguments from the #verify method call.")
- end
+ def verify!
reconnect! unless active?
end
@@ -478,6 +504,8 @@ module ActiveRecord
m.alias_type %r(number)i, "decimal"
m.alias_type %r(double)i, "float"
+ m.register_type %r(^json)i, Type::Json.new
+
m.register_type(%r(decimal)i) do |sql_type|
scale = extract_scale(sql_type)
precision = extract_precision(sql_type)
@@ -522,12 +550,7 @@ module ActiveRecord
end
def extract_limit(sql_type)
- case sql_type
- when /^bigint/i
- 8
- when /\((.*)\)/
- $1.to_i
- end
+ $1.to_i if sql_type =~ /\((.*)\)/
end
def translate_exception_class(e, sql)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
index 7cd086084a..07acb5425e 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -1,17 +1,15 @@
# frozen_string_literal: true
-require_relative "abstract_adapter"
-require_relative "statement_pool"
-require_relative "mysql/column"
-require_relative "mysql/explain_pretty_printer"
-require_relative "mysql/quoting"
-require_relative "mysql/schema_creation"
-require_relative "mysql/schema_definitions"
-require_relative "mysql/schema_dumper"
-require_relative "mysql/schema_statements"
-require_relative "mysql/type_metadata"
-
-require "active_support/core_ext/string/strip"
+require "active_record/connection_adapters/abstract_adapter"
+require "active_record/connection_adapters/statement_pool"
+require "active_record/connection_adapters/mysql/column"
+require "active_record/connection_adapters/mysql/explain_pretty_printer"
+require "active_record/connection_adapters/mysql/quoting"
+require "active_record/connection_adapters/mysql/schema_creation"
+require "active_record/connection_adapters/mysql/schema_definitions"
+require "active_record/connection_adapters/mysql/schema_dumper"
+require "active_record/connection_adapters/mysql/schema_statements"
+require "active_record/connection_adapters/mysql/type_metadata"
module ActiveRecord
module ConnectionAdapters
@@ -44,9 +42,9 @@ module ActiveRecord
json: { name: "json" },
}
- class StatementPool < ConnectionAdapters::StatementPool
+ class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
private def dealloc(stmt)
- stmt[:stmt].close
+ stmt.close
end
end
@@ -117,11 +115,11 @@ module ActiveRecord
end
def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
- query_value("SELECT GET_LOCK(#{quote(lock_name)}, #{timeout})") == 1
+ query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
end
def release_advisory_lock(lock_name) # :nodoc:
- query_value("SELECT RELEASE_LOCK(#{quote(lock_name)})") == 1
+ query_value("SELECT RELEASE_LOCK(#{quote(lock_name.to_s)})") == 1
end
def native_database_types
@@ -225,7 +223,7 @@ module ActiveRecord
end
end
- def empty_insert_statement_value
+ def empty_insert_statement_value(primary_key = nil)
"VALUES ()"
end
@@ -249,7 +247,7 @@ module ActiveRecord
# create_database 'matt_development', charset: :big5
def create_database(name, options = {})
if options[:collation]
- execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')} COLLATE #{quote_table_name(options[:collation])}"
+ execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT COLLATE #{quote_table_name(options[:collation])}"
else
execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}"
end
@@ -284,7 +282,7 @@ module ActiveRecord
def table_comment(table_name) # :nodoc:
scope = quoted_scope(table_name)
- query_value(<<-SQL.strip_heredoc, "SCHEMA")
+ query_value(<<~SQL, "SCHEMA").presence
SELECT table_comment
FROM information_schema.tables
WHERE table_schema = #{scope[:schema]}
@@ -292,14 +290,10 @@ module ActiveRecord
SQL
end
- def create_table(table_name, **options) #:nodoc:
- super(table_name, options: "ENGINE=InnoDB", **options)
- end
-
def bulk_change_table(table_name, operations) #:nodoc:
sqls = operations.flat_map do |command, args|
table, arguments = args.shift, args
- method = :"#{command}_sql"
+ method = :"#{command}_for_alter"
if respond_to?(method, true)
send(method, table, *arguments)
@@ -311,6 +305,11 @@ module ActiveRecord
execute("ALTER TABLE #{quote_table_name(table_name)} #{sqls}")
end
+ def change_table_comment(table_name, comment) #:nodoc:
+ comment = "" if comment.nil?
+ execute("ALTER TABLE #{quote_table_name(table_name)} COMMENT #{quote(comment)}")
+ end
+
# Renames a table.
#
# Example:
@@ -351,26 +350,27 @@ module ActiveRecord
def change_column_default(table_name, column_name, default_or_changes) #:nodoc:
default = extract_new_default_value(default_or_changes)
- column = column_for(table_name, column_name)
- change_column table_name, column_name, column.sql_type, default: default
+ change_column table_name, column_name, nil, default: default
end
def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
- column = column_for(table_name, column_name)
-
unless null || default.nil?
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
end
- change_column table_name, column_name, column.sql_type, null: null
+ change_column table_name, column_name, nil, null: null
+ end
+
+ def change_column_comment(table_name, column_name, comment) #:nodoc:
+ change_column table_name, column_name, nil, comment: comment
end
def change_column(table_name, column_name, type, options = {}) #:nodoc:
- execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_sql(table_name, column_name, type, options)}")
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{change_column_for_alter(table_name, column_name, type, options)}")
end
def rename_column(table_name, column_name, new_column_name) #:nodoc:
- execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
+ execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_for_alter(table_name, column_name, new_column_name)}")
rename_column_indexes(table_name, column_name, new_column_name)
end
@@ -390,19 +390,20 @@ module ActiveRecord
scope = quoted_scope(table_name)
- fk_info = exec_query(<<-SQL.strip_heredoc, "SCHEMA")
+ fk_info = exec_query(<<~SQL, "SCHEMA")
SELECT fk.referenced_table_name AS 'to_table',
fk.referenced_column_name AS 'primary_key',
fk.column_name AS 'column',
fk.constraint_name AS 'name',
rc.update_rule AS 'on_update',
rc.delete_rule AS 'on_delete'
- FROM information_schema.key_column_usage fk
- JOIN information_schema.referential_constraints rc
+ FROM information_schema.referential_constraints rc
+ JOIN information_schema.key_column_usage fk
USING (constraint_schema, constraint_name)
WHERE fk.referenced_column_name IS NOT NULL
AND fk.table_schema = #{scope[:schema]}
AND fk.table_name = #{scope[:name]}
+ AND rc.constraint_schema = #{scope[:schema]}
AND rc.table_name = #{scope[:name]}
SQL
@@ -477,7 +478,7 @@ module ActiveRecord
scope = quoted_scope(table_name)
- query_values(<<-SQL.strip_heredoc, "SCHEMA")
+ query_values(<<~SQL, "SCHEMA")
SELECT column_name
FROM information_schema.key_column_usage
WHERE constraint_name = 'PRIMARY'
@@ -512,7 +513,7 @@ module ActiveRecord
s.gsub(/\s+(?:ASC|DESC)\b/i, "")
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
- [super, *order_columns].join(", ")
+ (order_columns << super).join(", ")
end
def strict_mode?
@@ -523,23 +524,49 @@ module ActiveRecord
index.using == :btree || super
end
- def insert_fixtures(*)
- without_sql_mode("NO_AUTO_VALUE_ON_ZERO") { super }
+ def insert_fixtures_set(fixture_set, tables_to_delete = [])
+ with_multi_statements do
+ super { discard_remaining_results }
+ end
end
private
+ def combine_multi_statements(total_sql)
+ total_sql.each_with_object([]) do |sql, total_sql_chunks|
+ previous_packet = total_sql_chunks.last
+ sql << ";\n"
+ if max_allowed_packet_reached?(sql, previous_packet) || total_sql_chunks.empty?
+ total_sql_chunks << sql
+ else
+ previous_packet << sql
+ end
+ end
+ end
+
+ def max_allowed_packet_reached?(current_packet, previous_packet)
+ if current_packet.bytesize > max_allowed_packet
+ raise ActiveRecordError, "Fixtures set is too large #{current_packet.bytesize}. Consider increasing the max_allowed_packet variable."
+ elsif previous_packet.nil?
+ false
+ else
+ (current_packet.bytesize + previous_packet.bytesize) > max_allowed_packet
+ end
+ end
- def without_sql_mode(mode)
- result = execute("SELECT @@SESSION.sql_mode")
- current_mode = result.first[0]
- return yield unless current_mode.include?(mode)
+ def max_allowed_packet
+ bytes_margin = 2
+ @max_allowed_packet ||= (show_variable("max_allowed_packet") - bytes_margin)
+ end
+
+ def with_multi_statements
+ previous_flags = @config[:flags]
+ @config[:flags] = Mysql2::Client::MULTI_STATEMENTS
+ reconnect!
- sql_mode = "REPLACE(@@sql_mode, '#{mode}', '')"
- execute("SET @@SESSION.sql_mode = #{sql_mode}")
yield
ensure
- sql_mode = "CONCAT(@@sql_mode, ',#{mode}')"
- execute("SET @@SESSION.sql_mode = #{sql_mode}")
+ @config[:flags] = previous_flags
+ reconnect!
end
def initialize_type_map(m = type_map)
@@ -557,7 +584,6 @@ module ActiveRecord
m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1)
m.register_type %r(^float)i, Type::Float.new(limit: 24)
m.register_type %r(^double)i, Type::Float.new(limit: 53)
- m.register_type %r(^json)i, Type::Json.new
register_integer_type m, %r(^bigint)i, limit: 8
register_integer_type m, %r(^int)i, limit: 4
@@ -600,25 +626,6 @@ module ActiveRecord
end
end
- def add_index_length(quoted_columns, **options)
- if length = options[:length]
- case length
- when Hash
- length = length.symbolize_keys
- quoted_columns.each { |name, column| column << "(#{length[name]})" if length[name].present? }
- when Integer
- quoted_columns.each { |name, column| column << "(#{length})" }
- end
- end
-
- quoted_columns
- end
-
- def add_options_for_index_columns(quoted_columns, **options)
- quoted_columns = add_index_length(quoted_columns, options)
- super
- end
-
# See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html
ER_DUP_ENTRY = 1062
ER_NOT_NULL_VIOLATION = 1048
@@ -630,6 +637,8 @@ module ActiveRecord
ER_CANNOT_ADD_FOREIGN = 1215
ER_CANNOT_CREATE_TABLE = 1005
ER_LOCK_WAIT_TIMEOUT = 1205
+ ER_QUERY_INTERRUPTED = 1317
+ ER_QUERY_TIMEOUT = 3024
def translate_exception(exception, message)
case error_number(exception)
@@ -654,20 +663,19 @@ module ActiveRecord
when ER_LOCK_DEADLOCK
Deadlocked.new(message)
when ER_LOCK_WAIT_TIMEOUT
- TransactionTimeout.new(message)
+ LockWaitTimeout.new(message)
+ when ER_QUERY_TIMEOUT
+ StatementTimeout.new(message)
+ when ER_QUERY_INTERRUPTED
+ QueryCanceled.new(message)
else
super
end
end
- def add_column_sql(table_name, column_name, type, options = {})
- td = create_table_definition(table_name)
- cd = td.new_column_definition(column_name, type, options)
- schema_creation.accept(AddColumnDefinition.new(cd))
- end
-
- def change_column_sql(table_name, column_name, type, options = {})
+ def change_column_for_alter(table_name, column_name, type, options = {})
column = column_for(table_name, column_name)
+ type ||= column.sql_type
unless options.key?(:default)
options[:default] = column.default
@@ -686,7 +694,7 @@ module ActiveRecord
schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
end
- def rename_column_sql(table_name, column_name, new_column_name)
+ def rename_column_for_alter(table_name, column_name, new_column_name)
column = column_for(table_name, column_name)
options = {
default: column.default,
@@ -700,31 +708,23 @@ module ActiveRecord
schema_creation.accept(ChangeColumnDefinition.new(cd, column.name))
end
- def remove_column_sql(table_name, column_name, type = nil, options = {})
- "DROP #{quote_column_name(column_name)}"
- end
-
- def remove_columns_sql(table_name, *column_names)
- column_names.map { |column_name| remove_column_sql(table_name, column_name) }
- end
-
- def add_index_sql(table_name, column_name, options = {})
+ def add_index_for_alter(table_name, column_name, options = {})
index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options)
index_algorithm[0, 0] = ", " if index_algorithm.present?
"ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}"
end
- def remove_index_sql(table_name, options = {})
+ def remove_index_for_alter(table_name, options = {})
index_name = index_name_for_remove(table_name, options)
- "DROP INDEX #{index_name}"
+ "DROP INDEX #{quote_column_name(index_name)}"
end
- def add_timestamps_sql(table_name, options = {})
- [add_column_sql(table_name, :created_at, :datetime, options), add_column_sql(table_name, :updated_at, :datetime, options)]
+ def add_timestamps_for_alter(table_name, options = {})
+ [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)]
end
- def remove_timestamps_sql(table_name, options = {})
- [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)]
+ def remove_timestamps_for_alter(table_name, options = {})
+ [remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)]
end
# MySQL is too stupid to create a temporary table for use subquery, so we have
@@ -737,7 +737,8 @@ module ActiveRecord
# to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on'
subselect.distinct unless select.limit || select.offset || select.orders.any?
- Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key.name))
+ key_name = quote_column_name(key.name)
+ Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key_name))
end
def supports_rename_index?
diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb
index 16273fb5f1..5d81de9fe1 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -11,11 +11,11 @@ module ActiveRecord
# Instantiates a new column in the table.
#
- # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id int</tt>.
+ # +name+ is the column's name, such as <tt>supplier_id</tt> in <tt>supplier_id bigint</tt>.
# +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>.
# +sql_type_metadata+ is various information about the type of the column
# +null+ determines if this column allows +NULL+ values.
- def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment: nil)
+ def initialize(name, default, sql_type_metadata = nil, null = true, table_name = nil, default_function = nil, collation = nil, comment: nil, **)
@name = name.freeze
@table_name = table_name
@sql_type_metadata = sql_type_metadata
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index 29542f917e..901717ae3d 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -156,7 +156,6 @@ module ActiveRecord
env_config = config[env] if config[env].is_a?(Hash) && !(config[env].key?("adapter") || config[env].key?("url"))
end
- config.reject! { |k, v| v.is_a?(Hash) && !(v.key?("adapter") || v.key?("url")) }
config.merge! env_config if env_config
config.each do |key, value|
@@ -183,13 +182,25 @@ module ActiveRecord
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
+ # Require the adapter itself and give useful feedback about
+ # 1. Missing adapter gems and
+ # 2. Adapter gems' missing dependencies.
path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter"
begin
require path_to_adapter
- rescue Gem::LoadError => e
- raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)."
rescue LoadError => e
- raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace
+ # We couldn't require the adapter itself. Raise an exception that
+ # points out config typos and missing gems.
+ if e.path == path_to_adapter
+ # We can assume that a non-builtin adapter was specified, so it's
+ # either misspelled or missing from Gemfile.
+ raise e.class, "Could not load the '#{spec[:adapter]}' Active Record adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile.", e.backtrace
+
+ # Bubbled up from the adapter require. Prefix the exception message
+ # with some guidance about how to address it and reraise.
+ else
+ raise e.class, "Error loading the '#{spec[:adapter]}' Active Record adapter. Missing a gem it depends on? #{e.message}", e.backtrace
+ end
end
adapter_method = "#{spec[:adapter]}_connection"
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
index a058a72872..4106ce01be 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb
@@ -11,7 +11,7 @@ module ActiveRecord
else
super
end
- @connection.next_result while @connection.more_results?
+ discard_remaining_results
result
end
@@ -50,11 +50,18 @@ module ActiveRecord
alias :exec_update :exec_delete
private
+ def default_insert_value(column)
+ Arel.sql("DEFAULT") unless column.auto_increment?
+ end
def last_inserted_id(result)
@connection.last_id
end
+ def discard_remaining_results
+ @connection.abandon_results!
+ end
+
def exec_stmt_and_free(sql, name, binds, cache_stmt: false)
# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
# made since we established the connection
@@ -64,10 +71,7 @@ module ActiveRecord
log(sql, name, binds, type_casted_binds) do
if cache_stmt
- cache = @statements[sql] ||= {
- stmt: @connection.prepare(sql)
- }
- stmt = cache[:stmt]
+ stmt = @statements[sql] ||= @connection.prepare(sql)
else
stmt = @connection.prepare(sql)
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
index 75377693c6..c9ea653b77 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb
@@ -4,8 +4,7 @@ module ActiveRecord
module ConnectionAdapters
module MySQL
class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc:
- delegate :add_sql_comment!, :mariadb?, to: :@conn
- private :add_sql_comment!, :mariadb?
+ delegate :add_sql_comment!, :mariadb?, to: :@conn, private: true
private
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
index b22a2e4da7..2ed4ad16ae 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb
@@ -4,11 +4,6 @@ module ActiveRecord
module ConnectionAdapters
module MySQL
module ColumnMethods
- def primary_key(name, type = :primary_key, **options)
- options[:auto_increment] = true if [:integer, :bigint].include?(type) && !options.key?(:default)
- super
- end
-
def blob(*args, **options)
args.each { |name| column(name, :blob, options) }
end
@@ -37,10 +32,6 @@ module ActiveRecord
args.each { |name| column(name, :longtext, options) }
end
- def json(*args, **options)
- args.each { |name| column(name, :json, options) }
- end
-
def unsigned_integer(*args, **options)
args.each { |name| column(name, :unsigned_integer, options) }
end
@@ -68,7 +59,6 @@ module ActiveRecord
when :primary_key
type = :integer
options[:limit] ||= 8
- options[:auto_increment] = true
options[:primary_key] = true
when /\Aunsigned_(?<type>.+)\z/
type = $~[:type].to_sym
@@ -82,6 +72,11 @@ module ActiveRecord
def aliased_types(name, fallback)
fallback
end
+
+ def integer_like_primary_key_type(type, options)
+ options[:auto_increment] = true
+ type
+ end
end
class Table < ActiveRecord::ConnectionAdapters::Table
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
index 95eb77aea4..d23178e43c 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb
@@ -8,6 +8,7 @@ module ActiveRecord
def prepare_column_options(column)
spec = super
spec[:unsigned] = "true" if column.unsigned?
+ spec[:auto_increment] = "true" if column.auto_increment?
if @connection.supports_virtual_columns? && column.virtual?
spec[:as] = extract_expression_for_virtual_column(column)
@@ -18,6 +19,12 @@ module ActiveRecord
spec
end
+ def column_spec_for_primary_key(column)
+ spec = super
+ spec.delete(:auto_increment) if column.type == :integer && column.auto_increment?
+ spec
+ end
+
def default_primary_key?(column)
super && column.auto_increment? && !column.unsigned?
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
index 759493e3bd..ce50590651 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb
@@ -5,13 +5,7 @@ module ActiveRecord
module MySQL
module SchemaStatements # :nodoc:
# Returns an array of indexes for the given table.
- def indexes(table_name, name = nil)
- if name
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing name to #indexes is deprecated without replacement.
- MSG
- end
-
+ def indexes(table_name)
indexes = []
current_index = nil
execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result|
@@ -28,23 +22,26 @@ module ActiveRecord
index_using = mysql_index_type
end
- indexes << IndexDefinition.new(
+ indexes << [
row[:Table],
row[:Key_name],
row[:Non_unique].to_i == 0,
+ [],
+ lengths: {},
+ orders: {},
type: index_type,
using: index_using,
comment: row[:Index_comment].presence
- )
+ ]
end
- indexes.last.columns << row[:Column_name]
- indexes.last.lengths.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part]
- indexes.last.orders.merge!(row[:Column_name] => :desc) if row[:Collation] == "D"
+ indexes.last[-2] << row[:Column_name]
+ indexes.last[-1][:lengths].merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part]
+ indexes.last[-1][:orders].merge!(row[:Column_name] => :desc) if row[:Collation] == "D"
end
end
- indexes
+ indexes.map { |index| IndexDefinition.new(*index) }
end
def remove_column(table_name, column_name, type = nil, options = {})
@@ -109,6 +106,18 @@ module ActiveRecord
super unless specifier == "RESTRICT"
end
+ def add_index_length(quoted_columns, **options)
+ lengths = options_for_index_columns(options[:length])
+ quoted_columns.each do |name, column|
+ column << "(#{lengths[name]})" if lengths[name].present?
+ end
+ end
+
+ def add_options_for_index_columns(quoted_columns, **options)
+ quoted_columns = add_index_length(quoted_columns, options)
+ super
+ end
+
def data_source_sql(name = nil, type: nil)
scope = quoted_scope(name, type: type)
diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
index 2c2321872d..4c57bd48ab 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,11 +1,10 @@
# frozen_string_literal: true
-require_relative "abstract_mysql_adapter"
-require_relative "mysql/database_statements"
+require "active_record/connection_adapters/abstract_mysql_adapter"
+require "active_record/connection_adapters/mysql/database_statements"
-gem "mysql2", ">= 0.3.18", "< 0.5"
+gem "mysql2", ">= 0.4.4", "< 0.6.0"
require "mysql2"
-raise "mysql2 0.4.3 is not supported. Please upgrade to 0.4.4+" if Mysql2::VERSION == "0.4.3"
module ActiveRecord
module ConnectionHandling # :nodoc:
@@ -105,6 +104,11 @@ module ActiveRecord
@connection.close
end
+ def discard! # :nodoc:
+ @connection.automatic_close = false
+ @connection = nil
+ end
+
private
def connect
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
index ff95fa4a0e..3ccc7271ab 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb
@@ -7,6 +7,11 @@ module ActiveRecord
delegate :array, :oid, :fmod, to: :sql_type_metadata
alias :array? :array
+ def initialize(*, max_identifier_length: 63, **)
+ super
+ @max_identifier_length = max_identifier_length
+ end
+
def serial?
return unless default_function
@@ -16,7 +21,21 @@ module ActiveRecord
end
private
+ attr_reader :max_identifier_length
+
def sequence_name_from_parts(table_name, column_name, suffix)
+ over_length = [table_name, column_name, suffix].map(&:length).sum + 2 - max_identifier_length
+
+ if over_length > 0
+ column_name_length = [(max_identifier_length - suffix.length - 2) / 2, column_name.length].min
+ over_length -= column_name.length - column_name_length
+ column_name = column_name[0, column_name_length - [over_length, 0].min]
+ end
+
+ if over_length > 0
+ table_name = table_name[0, table_name.length - over_length]
+ end
+
"#{table_name}_#{column_name}_#{suffix}"
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index b28418d74f..247a25054e 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -1,27 +1,28 @@
# frozen_string_literal: true
-require_relative "oid/array"
-require_relative "oid/bit"
-require_relative "oid/bit_varying"
-require_relative "oid/bytea"
-require_relative "oid/cidr"
-require_relative "oid/date_time"
-require_relative "oid/decimal"
-require_relative "oid/enum"
-require_relative "oid/hstore"
-require_relative "oid/inet"
-require_relative "oid/jsonb"
-require_relative "oid/money"
-require_relative "oid/oid"
-require_relative "oid/point"
-require_relative "oid/legacy_point"
-require_relative "oid/range"
-require_relative "oid/specialized_string"
-require_relative "oid/uuid"
-require_relative "oid/vector"
-require_relative "oid/xml"
+require "active_record/connection_adapters/postgresql/oid/array"
+require "active_record/connection_adapters/postgresql/oid/bit"
+require "active_record/connection_adapters/postgresql/oid/bit_varying"
+require "active_record/connection_adapters/postgresql/oid/bytea"
+require "active_record/connection_adapters/postgresql/oid/cidr"
+require "active_record/connection_adapters/postgresql/oid/date"
+require "active_record/connection_adapters/postgresql/oid/date_time"
+require "active_record/connection_adapters/postgresql/oid/decimal"
+require "active_record/connection_adapters/postgresql/oid/enum"
+require "active_record/connection_adapters/postgresql/oid/hstore"
+require "active_record/connection_adapters/postgresql/oid/inet"
+require "active_record/connection_adapters/postgresql/oid/jsonb"
+require "active_record/connection_adapters/postgresql/oid/money"
+require "active_record/connection_adapters/postgresql/oid/oid"
+require "active_record/connection_adapters/postgresql/oid/point"
+require "active_record/connection_adapters/postgresql/oid/legacy_point"
+require "active_record/connection_adapters/postgresql/oid/range"
+require "active_record/connection_adapters/postgresql/oid/specialized_string"
+require "active_record/connection_adapters/postgresql/oid/uuid"
+require "active_record/connection_adapters/postgresql/oid/vector"
+require "active_record/connection_adapters/postgresql/oid/xml"
-require_relative "oid/type_map_initializer"
+require "active_record/connection_adapters/postgresql/oid/type_map_initializer"
module ActiveRecord
module ConnectionAdapters
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
index 587e95d192..e9a79526f9 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb
@@ -43,10 +43,7 @@ module ActiveRecord
/\A[0-9A-F]*\Z/i.match?(value)
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :value
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
new file mode 100644
index 0000000000..24a1daa95a
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module ConnectionAdapters
+ module PostgreSQL
+ module OID # :nodoc:
+ class Date < Type::Date # :nodoc:
+ def cast_value(value)
+ case value
+ when "infinity" then ::Float::INFINITY
+ when "-infinity" then -::Float::INFINITY
+ when / BC$/
+ astronomical_year = format("%04d", -value[/^\d+/].to_i + 1)
+ super(value.sub(/ BC$/, "").sub(/^\d+/, astronomical_year))
+ else
+ super
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
index 879dba7afd..e7d33855c4 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb
@@ -6,7 +6,7 @@ module ActiveRecord
module OID # :nodoc:
class Decimal < Type::Decimal # :nodoc:
def infinity(options = {})
- BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1)
+ BigDecimal("Infinity") * (options[:negative] ? -1 : 1)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
index 7d5d7d91e6..6edb7cfd3c 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb
@@ -35,7 +35,7 @@ module ActiveRecord
if value.is_a?(::Range)
from = type_cast_single_for_database(value.begin)
to = type_cast_single_for_database(value.end)
- "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}"
+ ::Range.new(from, to, value.exclude_end?)
else
super
end
@@ -60,7 +60,7 @@ module ActiveRecord
end
def type_cast_single_for_database(value)
- infinity?(value) ? "" : @subtype.serialize(value)
+ infinity?(value) ? value : @subtype.serialize(value)
end
def extract_bounds(value)
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index a0a22ba0f1..e75202b0be 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -101,6 +101,8 @@ module ActiveRecord
end
when OID::Array::Data
_quote(encode_array(value))
+ when Range
+ _quote(encode_range(value))
else
super
end
@@ -117,6 +119,8 @@ module ActiveRecord
value.to_s
when OID::Array::Data
encode_array(value)
+ when Range
+ encode_range(value)
else
super
end
@@ -133,6 +137,10 @@ module ActiveRecord
result
end
+ def encode_range(range)
+ "[#{type_cast_range_value(range.first)},#{type_cast_range_value(range.last)}#{range.exclude_end? ? ')' : ']'}"
+ end
+
def determine_encoding_of_strings_in_array(value)
case value
when ::Array then determine_encoding_of_strings_in_array(value.first)
@@ -146,6 +154,14 @@ module ActiveRecord
else _type_cast(values)
end
end
+
+ def type_cast_range_value(value)
+ infinity?(value) ? "" : type_cast(value)
+ end
+
+ def infinity?(value)
+ value.respond_to?(:infinite?) && value.infinite?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
index 386d22a9bd..8df91c988b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
@@ -4,26 +4,21 @@ module ActiveRecord
module ConnectionAdapters
module PostgreSQL
module ReferentialIntegrity # :nodoc:
- def supports_disable_referential_integrity? # :nodoc:
- true
- end
-
def disable_referential_integrity # :nodoc:
- if supports_disable_referential_integrity?
- original_exception = nil
+ original_exception = nil
- begin
- transaction(requires_new: true) do
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
- end
- rescue ActiveRecord::ActiveRecordError => e
- original_exception = e
+ begin
+ transaction(requires_new: true) do
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
end
+ rescue ActiveRecord::ActiveRecordError => e
+ original_exception = e
+ end
- begin
- yield
- rescue ActiveRecord::InvalidForeignKey => e
- warn <<-WARNING
+ begin
+ yield
+ rescue ActiveRecord::InvalidForeignKey => e
+ warn <<-WARNING
WARNING: Rails was not able to disable referential integrity.
This is most likely caused due to missing permissions.
@@ -32,17 +27,14 @@ Rails needs superuser privileges to disable referential integrity.
cause: #{original_exception.try(:message)}
WARNING
- raise e
- end
+ raise e
+ end
- begin
- transaction(requires_new: true) do
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
- end
- rescue ActiveRecord::ActiveRecordError
+ begin
+ transaction(requires_new: true) do
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
end
- else
- yield
+ rescue ActiveRecord::ActiveRecordError
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
index 59f661da25..8e381a92cf 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb
@@ -5,6 +5,18 @@ module ActiveRecord
module PostgreSQL
class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc:
private
+ def visit_AlterTable(o)
+ super << o.constraint_validations.map { |fk| visit_ValidateConstraint fk }.join(" ")
+ end
+
+ def visit_AddForeignKey(o)
+ super.dup.tap { |sql| sql << " NOT VALID" unless o.validate? }
+ end
+
+ def visit_ValidateConstraint(name)
+ "VALIDATE CONSTRAINT #{quote_column_name(name)}"
+ end
+
def add_column_options!(sql, options)
if options[:collation]
sql << " COLLATE \"#{options[:collation]}\""
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
index f1489e4d69..6047217fcd 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb
@@ -44,15 +44,8 @@ module ActiveRecord
# a record (as primary keys cannot be +nil+). This might be done via the
# +SecureRandom.uuid+ method and a +before_save+ callback, for instance.
def primary_key(name, type = :primary_key, **options)
- options[:auto_increment] = true if [:integer, :bigint].include?(type) && !options.key?(:default)
if type == :uuid
options[:default] = options.fetch(:default, "gen_random_uuid()")
- elsif options.delete(:auto_increment) == true && %i(integer bigint).include?(type)
- type = if type == :bigint || options[:limit] == 8
- :bigserial
- else
- :serial
- end
end
super
@@ -102,10 +95,6 @@ module ActiveRecord
args.each { |name| column(name, :int8range, options) }
end
- def json(*args, **options)
- args.each { |name| column(name, :json, options) }
- end
-
def jsonb(*args, **options)
args.each { |name| column(name, :jsonb, options) }
end
@@ -185,11 +174,33 @@ module ActiveRecord
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
include ColumnMethods
+
+ private
+ def integer_like_primary_key_type(type, options)
+ if type == :bigint || options[:limit] == 8
+ :bigserial
+ else
+ :serial
+ end
+ end
end
class Table < ActiveRecord::ConnectionAdapters::Table
include ColumnMethods
end
+
+ class AlterTable < ActiveRecord::ConnectionAdapters::AlterTable
+ attr_reader :constraint_validations
+
+ def initialize(td)
+ super
+ @constraint_validations = []
+ end
+
+ def validate_constraint(name)
+ @constraint_validations << name
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
index c0dbb166b7..84643d20da 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb
@@ -5,6 +5,18 @@ module ActiveRecord
module PostgreSQL
class SchemaDumper < ConnectionAdapters::SchemaDumper # :nodoc:
private
+
+ def extensions(stream)
+ extensions = @connection.extensions
+ if extensions.any?
+ stream.puts " # These are extensions that must be enabled in order to support this database"
+ extensions.sort.each do |extension|
+ stream.puts " enable_extension #{extension.inspect}"
+ end
+ stream.puts
+ end
+ end
+
def prepare_column_options(column)
spec = super
spec[:array] = "true" if column.array?
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
index 9e2f61e6ce..e20e5f2914 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -1,7 +1,5 @@
# frozen_string_literal: true
-require "active_support/core_ext/string/strip"
-
module ActiveRecord
module ConnectionAdapters
module PostgreSQL
@@ -40,7 +38,7 @@ module ActiveRecord
" TABLESPACE = \"#{value}\""
when :connection_limit
" CONNECTION LIMIT = #{value}"
- else
+ else
""
end
end
@@ -66,12 +64,7 @@ module ActiveRecord
end
# Verifies existence of an index with a given name.
- def index_name_exists?(table_name, index_name, default = nil)
- unless default.nil?
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing default to #index_name_exists? is deprecated without replacement.
- MSG
- end
+ def index_name_exists?(table_name, index_name)
table = quoted_scope(table_name)
index = quoted_scope(index_name)
@@ -89,21 +82,12 @@ module ActiveRecord
end
# Returns an array of indexes for the given table.
- def indexes(table_name, name = nil) # :nodoc:
- if name
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing name to #indexes is deprecated without replacement.
- MSG
- end
-
+ def indexes(table_name) # :nodoc:
scope = quoted_scope(table_name)
result = query(<<-SQL, "SCHEMA")
SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid,
- pg_catalog.obj_description(i.oid, 'pg_class') AS comment,
- (SELECT COUNT(*) FROM pg_opclass o
- JOIN (SELECT unnest(string_to_array(d.indclass::text, ' '))::int oid) c
- ON o.oid = c.oid WHERE o.opcdefault = 'f')
+ pg_catalog.obj_description(i.oid, 'pg_class') AS comment
FROM pg_class t
INNER JOIN pg_index d ON t.oid = d.indrelid
INNER JOIN pg_class i ON d.indexrelid = i.oid
@@ -122,24 +106,32 @@ module ActiveRecord
inddef = row[3]
oid = row[4]
comment = row[5]
- opclass = row[6]
- using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/).flatten
+ using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/m).flatten
- if indkey.include?(0) || opclass > 0
+ orders = {}
+ opclasses = {}
+
+ if indkey.include?(0)
columns = expressions
else
- columns = Hash[query(<<-SQL.strip_heredoc, "SCHEMA")].values_at(*indkey).compact
+ columns = Hash[query(<<~SQL, "SCHEMA")].values_at(*indkey).compact
SELECT a.attnum, a.attname
FROM pg_attribute a
WHERE a.attrelid = #{oid}
AND a.attnum IN (#{indkey.join(",")})
SQL
- # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
- orders = Hash[
- expressions.scan(/(\w+) DESC/).flatten.map { |order_column| [order_column, :desc] }
- ]
+ # add info on sort order (only desc order is explicitly specified, asc is the default)
+ # and non-default opclasses
+ expressions.scan(/(?<column>\w+)\s?(?<opclass>\w+_ops)?\s?(?<desc>DESC)?\s?(?<nulls>NULLS (?:FIRST|LAST))?/).each do |column, opclass, desc, nulls|
+ opclasses[column] = opclass.to_sym if opclass
+ if nulls
+ orders[column] = [desc, nulls].compact.join(" ")
+ else
+ orders[column] = :desc if desc
+ end
+ end
end
IndexDefinition.new(
@@ -148,6 +140,7 @@ module ActiveRecord
unique,
columns,
orders: orders,
+ opclasses: opclasses,
where: where,
using: using.to_sym,
comment: comment.presence
@@ -165,7 +158,7 @@ module ActiveRecord
def table_comment(table_name) # :nodoc:
scope = quoted_scope(table_name, type: "BASE TABLE")
if scope[:name]
- query_value(<<-SQL.strip_heredoc, "SCHEMA")
+ query_value(<<~SQL, "SCHEMA")
SELECT pg_catalog.obj_description(c.oid, 'pg_class')
FROM pg_catalog.pg_class c
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
@@ -360,7 +353,7 @@ module ActiveRecord
end
def primary_keys(table_name) # :nodoc:
- query_values(<<-SQL.strip_heredoc, "SCHEMA")
+ query_values(<<~SQL, "SCHEMA")
SELECT a.attname
FROM (
SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx
@@ -375,6 +368,31 @@ module ActiveRecord
SQL
end
+ def bulk_change_table(table_name, operations)
+ sql_fragments = []
+ non_combinable_operations = []
+
+ operations.each do |command, args|
+ table, arguments = args.shift, args
+ method = :"#{command}_for_alter"
+
+ if respond_to?(method, true)
+ sqls, procs = Array(send(method, table, *arguments)).partition { |v| v.is_a?(String) }
+ sql_fragments << sqls
+ non_combinable_operations.concat(procs)
+ else
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
+ non_combinable_operations.each(&:call)
+ sql_fragments = []
+ non_combinable_operations = []
+ send(command, table, *arguments)
+ end
+ end
+
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sql_fragments.join(", ")}" unless sql_fragments.empty?
+ non_combinable_operations.each(&:call)
+ end
+
# Renames a table.
# Also renames a table's primary key sequence if the sequence name exists and
# matches the Active Record default.
@@ -405,50 +423,23 @@ module ActiveRecord
def change_column(table_name, column_name, type, options = {}) #:nodoc:
clear_cache!
- quoted_table_name = quote_table_name(table_name)
- quoted_column_name = quote_column_name(column_name)
- sql_type = type_to_sql(type, options)
- sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}".dup
- if options[:collation]
- sql << " COLLATE \"#{options[:collation]}\""
- end
- if options[:using]
- sql << " USING #{options[:using]}"
- elsif options[:cast_as]
- cast_as_type = type_to_sql(options[:cast_as], options)
- sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})"
- end
- execute sql
-
- change_column_default(table_name, column_name, options[:default]) if options.key?(:default)
- change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
- change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
+ sqls, procs = Array(change_column_for_alter(table_name, column_name, type, options)).partition { |v| v.is_a?(String) }
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sqls.join(", ")}"
+ procs.each(&:call)
end
# Changes the default value of a table column.
def change_column_default(table_name, column_name, default_or_changes) # :nodoc:
- clear_cache!
- column = column_for(table_name, column_name)
- return unless column
-
- default = extract_new_default_value(default_or_changes)
- alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s"
- if default.nil?
- # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will
- # cast the default to the columns type, which leaves us with a default like "default NULL::character varying".
- execute alter_column_query % "DROP DEFAULT"
- else
- execute alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}"
- end
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_default_for_alter(table_name, column_name, default_or_changes)}"
end
def change_column_null(table_name, column_name, null, default = nil) #:nodoc:
clear_cache!
unless null || default.nil?
column = column_for(table_name, column_name)
- execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL") if column
+ execute "UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote_default_expression(default, column)} WHERE #{quote_column_name(column_name)} IS NULL" if column
end
- execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL")
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{change_column_null_for_alter(table_name, column_name, null, default)}"
end
# Adds comment for given table column or drops it if +comment+ is a +nil+
@@ -471,8 +462,8 @@ module ActiveRecord
end
def add_index(table_name, column_name, options = {}) #:nodoc:
- index_name, index_type, index_columns, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options)
- execute("CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}").tap do
+ index_name, index_type, index_columns_and_opclasses, index_options, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options)
+ execute("CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns_and_opclasses})#{index_options}").tap do
execute "COMMENT ON INDEX #{quote_column_name(index_name)} IS #{quote(comment)}" if comment
end
end
@@ -511,8 +502,8 @@ module ActiveRecord
def foreign_keys(table_name)
scope = quoted_scope(table_name)
- fk_info = exec_query(<<-SQL.strip_heredoc, "SCHEMA")
- SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete
+ fk_info = exec_query(<<~SQL, "SCHEMA")
+ SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete, c.convalidated AS valid
FROM pg_constraint c
JOIN pg_class t1 ON c.conrelid = t1.oid
JOIN pg_class t2 ON c.confrelid = t2.oid
@@ -534,11 +525,20 @@ module ActiveRecord
options[:on_delete] = extract_foreign_key_action(row["on_delete"])
options[:on_update] = extract_foreign_key_action(row["on_update"])
+ options[:validate] = row["valid"]
ForeignKeyDefinition.new(table_name, row["to_table"], options)
end
end
+ def foreign_tables
+ query_values(data_source_sql(type: "FOREIGN TABLE"), "SCHEMA")
+ end
+
+ def foreign_table_exists?(table_name)
+ query_values(data_source_sql(table_name, type: "FOREIGN TABLE"), "SCHEMA").any? if table_name.present?
+ end
+
# Maps logical Rails types to PostgreSQL-specific data types.
def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc:
sql = \
@@ -583,7 +583,7 @@ module ActiveRecord
.gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, "")
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
- [super, *order_columns].join(", ")
+ (order_columns << super).join(", ")
end
def update_table_definition(table_name, base) # :nodoc:
@@ -594,6 +594,43 @@ module ActiveRecord
PostgreSQL::SchemaDumper.create(self, options)
end
+ # Validates the given constraint.
+ #
+ # Validates the constraint named +constraint_name+ on +accounts+.
+ #
+ # validate_constraint :accounts, :constraint_name
+ def validate_constraint(table_name, constraint_name)
+ return unless supports_validate_constraints?
+
+ at = create_alter_table table_name
+ at.validate_constraint constraint_name
+
+ execute schema_creation.accept(at)
+ end
+
+ # Validates the given foreign key.
+ #
+ # Validates the foreign key on +accounts.branch_id+.
+ #
+ # validate_foreign_key :accounts, :branches
+ #
+ # Validates the foreign key on +accounts.owner_id+.
+ #
+ # validate_foreign_key :accounts, column: :owner_id
+ #
+ # Validates the foreign key named +special_fk_name+ on the +accounts+ table.
+ #
+ # validate_foreign_key :accounts, name: :special_fk_name
+ #
+ # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key.
+ def validate_foreign_key(from_table, options_or_to_table = {})
+ return unless supports_validate_constraints?
+
+ fk_name_to_validate = foreign_key_for!(from_table, options_or_to_table).name
+
+ validate_constraint from_table, fk_name_to_validate
+ end
+
private
def schema_creation
PostgreSQL::SchemaCreation.new(self)
@@ -603,6 +640,10 @@ module ActiveRecord
PostgreSQL::TableDefinition.new(*args)
end
+ def create_alter_table(name)
+ PostgreSQL::AlterTable.new create_table_definition(name)
+ end
+
def new_column_from_field(table_name, field)
column_name, type, default, notnull, oid, fmod, collation, comment = field
type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i)
@@ -617,7 +658,8 @@ module ActiveRecord
table_name,
default_function,
collation,
- comment: comment.presence
+ comment: comment.presence,
+ max_identifier_length: max_identifier_length
)
end
@@ -641,9 +683,75 @@ module ActiveRecord
end
end
+ def change_column_sql(table_name, column_name, type, options = {})
+ quoted_column_name = quote_column_name(column_name)
+ sql_type = type_to_sql(type, options)
+ sql = "ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}".dup
+ if options[:collation]
+ sql << " COLLATE \"#{options[:collation]}\""
+ end
+ if options[:using]
+ sql << " USING #{options[:using]}"
+ elsif options[:cast_as]
+ cast_as_type = type_to_sql(options[:cast_as], options)
+ sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})"
+ end
+
+ sql
+ end
+
+ def change_column_for_alter(table_name, column_name, type, options = {})
+ sqls = [change_column_sql(table_name, column_name, type, options)]
+ sqls << change_column_default_for_alter(table_name, column_name, options[:default]) if options.key?(:default)
+ sqls << change_column_null_for_alter(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
+ sqls << Proc.new { change_column_comment(table_name, column_name, options[:comment]) } if options.key?(:comment)
+ sqls
+ end
+
+
+ # Changes the default value of a table column.
+ def change_column_default_for_alter(table_name, column_name, default_or_changes) # :nodoc:
+ column = column_for(table_name, column_name)
+ return unless column
+
+ default = extract_new_default_value(default_or_changes)
+ alter_column_query = "ALTER COLUMN #{quote_column_name(column_name)} %s"
+ if default.nil?
+ # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will
+ # cast the default to the columns type, which leaves us with a default like "default NULL::character varying".
+ alter_column_query % "DROP DEFAULT"
+ else
+ alter_column_query % "SET DEFAULT #{quote_default_expression(default, column)}"
+ end
+ end
+
+ def change_column_null_for_alter(table_name, column_name, null, default = nil) #:nodoc:
+ "ALTER #{quote_column_name(column_name)} #{null ? 'DROP' : 'SET'} NOT NULL"
+ end
+
+ def add_timestamps_for_alter(table_name, options = {})
+ [add_column_for_alter(table_name, :created_at, :datetime, options), add_column_for_alter(table_name, :updated_at, :datetime, options)]
+ end
+
+ def remove_timestamps_for_alter(table_name, options = {})
+ [remove_column_for_alter(table_name, :updated_at), remove_column_for_alter(table_name, :created_at)]
+ end
+
+ def add_index_opclass(quoted_columns, **options)
+ opclasses = options_for_index_columns(options[:opclass])
+ quoted_columns.each do |name, column|
+ column << " #{opclasses[name]}" if opclasses[name].present?
+ end
+ end
+
+ def add_options_for_index_columns(quoted_columns, **options)
+ quoted_columns = add_index_opclass(quoted_columns, options)
+ super
+ end
+
def data_source_sql(name = nil, type: nil)
scope = quoted_scope(name, type: type)
- scope[:type] ||= "'r','v','m'" # (r)elation/table, (v)iew, (m)aterialized view
+ scope[:type] ||= "'r','v','m','f'" # (r)elation/table, (v)iew, (m)aterialized view, (f)oreign table
sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace".dup
sql << " WHERE n.nspname = #{scope[:schema]}"
@@ -660,6 +768,8 @@ module ActiveRecord
"'r'"
when "VIEW"
"'v','m'"
+ when "FOREIGN TABLE"
+ "'f'"
end
scope = {}
scope[:schema] = schema ? quote(schema) : "ANY (current_schemas(false))"
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
index 4d37a292d6..fdf6f75108 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -1,23 +1,23 @@
# frozen_string_literal: true
# Make sure we're using pg high enough for type casts and Ruby 2.2+ compatibility
-gem "pg", "~> 0.18"
+gem "pg", ">= 0.18", "< 2.0"
require "pg"
-require_relative "abstract_adapter"
-require_relative "statement_pool"
-require_relative "postgresql/column"
-require_relative "postgresql/database_statements"
-require_relative "postgresql/explain_pretty_printer"
-require_relative "postgresql/oid"
-require_relative "postgresql/quoting"
-require_relative "postgresql/referential_integrity"
-require_relative "postgresql/schema_creation"
-require_relative "postgresql/schema_definitions"
-require_relative "postgresql/schema_dumper"
-require_relative "postgresql/schema_statements"
-require_relative "postgresql/type_metadata"
-require_relative "postgresql/utils"
+require "active_record/connection_adapters/abstract_adapter"
+require "active_record/connection_adapters/statement_pool"
+require "active_record/connection_adapters/postgresql/column"
+require "active_record/connection_adapters/postgresql/database_statements"
+require "active_record/connection_adapters/postgresql/explain_pretty_printer"
+require "active_record/connection_adapters/postgresql/oid"
+require "active_record/connection_adapters/postgresql/quoting"
+require "active_record/connection_adapters/postgresql/referential_integrity"
+require "active_record/connection_adapters/postgresql/schema_creation"
+require "active_record/connection_adapters/postgresql/schema_definitions"
+require "active_record/connection_adapters/postgresql/schema_dumper"
+require "active_record/connection_adapters/postgresql/schema_statements"
+require "active_record/connection_adapters/postgresql/type_metadata"
+require "active_record/connection_adapters/postgresql/utils"
module ActiveRecord
module ConnectionHandling # :nodoc:
@@ -122,6 +122,10 @@ module ActiveRecord
include PostgreSQL::SchemaStatements
include PostgreSQL::DatabaseStatements
+ def supports_bulk_alter?
+ true
+ end
+
def supports_index_sort_order?
true
end
@@ -142,6 +146,10 @@ module ActiveRecord
true
end
+ def supports_validate_constraints?
+ true
+ end
+
def supports_views?
true
end
@@ -166,7 +174,7 @@ module ActiveRecord
{ concurrently: "CONCURRENTLY" }
end
- class StatementPool < ConnectionAdapters::StatementPool
+ class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
def initialize(connection, max)
super(max)
@connection = connection
@@ -182,7 +190,6 @@ module ActiveRecord
end
private
-
def dealloc(key)
@connection.query "DEALLOCATE #{key}" if connection_active?
rescue PG::Error
@@ -273,6 +280,11 @@ module ActiveRecord
end
end
+ def discard! # :nodoc:
+ @connection.socket_io.reopen(IO::NULL) rescue nil
+ @connection = nil
+ end
+
def native_database_types #:nodoc:
NATIVE_DATABASE_TYPES
end
@@ -306,20 +318,24 @@ module ActiveRecord
postgresql_version >= 90300
end
+ def supports_foreign_tables?
+ postgresql_version >= 90300
+ end
+
def supports_pgcrypto_uuid?
postgresql_version >= 90400
end
def get_advisory_lock(lock_id) # :nodoc:
unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63
- raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer")
+ raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer")
end
query_value("SELECT pg_try_advisory_lock(#{lock_id})")
end
def release_advisory_lock(lock_id) # :nodoc:
unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63
- raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer")
+ raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer")
end
query_value("SELECT pg_advisory_unlock(#{lock_id})")
end
@@ -337,25 +353,20 @@ module ActiveRecord
end
def extension_enabled?(name)
- if supports_extensions?
- res = exec_query("SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", "SCHEMA")
- res.cast_values.first
- end
+ res = exec_query("SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", "SCHEMA")
+ res.cast_values.first
end
def extensions
- if supports_extensions?
- exec_query("SELECT extname FROM pg_extension", "SCHEMA").cast_values
- else
- super
- end
+ exec_query("SELECT extname FROM pg_extension", "SCHEMA").cast_values
end
# Returns the configured supported identifier length supported by PostgreSQL
- def table_alias_length
+ def max_identifier_length
@max_identifier_length ||= query_value("SHOW max_identifier_length", "SCHEMA").to_i
end
- alias index_name_length table_alias_length
+ alias table_alias_length max_identifier_length
+ alias index_name_length max_identifier_length
# Set the authorized user for this session
def session_auth=(user)
@@ -387,7 +398,6 @@ module ActiveRecord
end
private
-
# See https://www.postgresql.org/docs/current/static/errcodes-appendix.html
VALUE_LIMIT_VIOLATION = "22001"
NUMERIC_VALUE_OUT_OF_RANGE = "22003"
@@ -396,6 +406,8 @@ module ActiveRecord
UNIQUE_VIOLATION = "23505"
SERIALIZATION_FAILURE = "40001"
DEADLOCK_DETECTED = "40P01"
+ LOCK_NOT_AVAILABLE = "55P03"
+ QUERY_CANCELED = "57014"
def translate_exception(exception, message)
return exception unless exception.respond_to?(:result)
@@ -415,6 +427,10 @@ module ActiveRecord
SerializationFailure.new(message)
when DEADLOCK_DETECTED
Deadlocked.new(message)
+ when LOCK_NOT_AVAILABLE
+ LockWaitTimeout.new(message)
+ when QUERY_CANCELED
+ QueryCanceled.new(message)
else
super
end
@@ -449,7 +465,7 @@ module ActiveRecord
register_class_with_limit m, "bit", OID::Bit
register_class_with_limit m, "varbit", OID::BitVarying
m.alias_type "timestamptz", "timestamp"
- m.register_type "date", Type::Date.new
+ m.register_type "date", OID::Date.new
m.register_type "money", OID::Money.new
m.register_type "bytea", OID::Bytea.new
@@ -821,6 +837,7 @@ module ActiveRecord
ActiveRecord::Type.register(:bit_varying, OID::BitVarying, adapter: :postgresql)
ActiveRecord::Type.register(:binary, OID::Bytea, adapter: :postgresql)
ActiveRecord::Type.register(:cidr, OID::Cidr, adapter: :postgresql)
+ ActiveRecord::Type.register(:date, OID::Date, adapter: :postgresql)
ActiveRecord::Type.register(:datetime, OID::DateTime, adapter: :postgresql)
ActiveRecord::Type.register(:decimal, OID::Decimal, adapter: :postgresql)
ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql)
diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
index f34b6733da..c29cf1f9a1 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -28,7 +28,7 @@ module ActiveRecord
coder["columns_hash"] = @columns_hash
coder["primary_keys"] = @primary_keys
coder["data_sources"] = @data_sources
- coder["version"] = ActiveRecord::Migrator.current_version
+ coder["version"] = connection.migration_context.current_version
end
def init_with(coder)
@@ -100,7 +100,7 @@ module ActiveRecord
def marshal_dump
# if we get current version during initialization, it happens stack over flow.
- @version = ActiveRecord::Migrator.current_version
+ @version = connection.migration_context.current_version
[@version, @columns, @columns_hash, @primary_keys, @data_sources]
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
index 8042dbfea2..70de96326c 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb
@@ -17,7 +17,7 @@ module ActiveRecord
end
def quoted_time(value)
- quoted_date(value)
+ quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ")
end
def quoted_binary(value)
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb
index 501f17dbad..c9855019c1 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb
@@ -3,27 +3,16 @@
module ActiveRecord
module ConnectionAdapters
module SQLite3
- module ColumnMethods
- def primary_key(name, type = :primary_key, **options)
- if %i(integer bigint).include?(type) && (options.delete(:auto_increment) == true || !options.key?(:default))
- type = :primary_key
- end
-
- super
- end
- end
-
class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
- include ColumnMethods
-
def references(*args, **options)
super(*args, type: :integer, **options)
end
alias :belongs_to :references
- end
- class Table < ActiveRecord::ConnectionAdapters::Table
- include ColumnMethods
+ private
+ def integer_like_primary_key_type(type, options)
+ :primary_key
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
index a512702b7b..58e5138e02 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb
@@ -5,13 +5,7 @@ module ActiveRecord
module SQLite3
module SchemaStatements # :nodoc:
# Returns an array of indexes for the given table.
- def indexes(table_name, name = nil)
- if name
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing name to #indexes is deprecated without replacement.
- MSG
- end
-
+ def indexes(table_name)
exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row|
index_sql = query_value(<<-SQL, "SCHEMA")
SELECT sql
@@ -29,20 +23,26 @@ module ActiveRecord
col["name"]
end
+ # Add info on sort order for columns (only desc order is explicitly specified, asc is
+ # the default)
+ orders = {}
+ if index_sql # index_sql can be null in case of primary key indexes
+ index_sql.scan(/"(\w+)" DESC/).flatten.each { |order_column|
+ orders[order_column] = :desc
+ }
+ end
+
IndexDefinition.new(
table_name,
row["name"],
row["unique"] != 0,
columns,
- where: where
+ where: where,
+ orders: orders
)
end
end
- def update_table_definition(table_name, base)
- SQLite3::Table.new(table_name, base)
- end
-
def create_schema_dumper(options)
SQLite3::SchemaDumper.create(self, options)
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 6fdd666486..800e731f06 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -1,13 +1,13 @@
# frozen_string_literal: true
-require_relative "abstract_adapter"
-require_relative "statement_pool"
-require_relative "sqlite3/explain_pretty_printer"
-require_relative "sqlite3/quoting"
-require_relative "sqlite3/schema_creation"
-require_relative "sqlite3/schema_definitions"
-require_relative "sqlite3/schema_dumper"
-require_relative "sqlite3/schema_statements"
+require "active_record/connection_adapters/abstract_adapter"
+require "active_record/connection_adapters/statement_pool"
+require "active_record/connection_adapters/sqlite3/explain_pretty_printer"
+require "active_record/connection_adapters/sqlite3/quoting"
+require "active_record/connection_adapters/sqlite3/schema_creation"
+require "active_record/connection_adapters/sqlite3/schema_definitions"
+require "active_record/connection_adapters/sqlite3/schema_dumper"
+require "active_record/connection_adapters/sqlite3/schema_statements"
gem "sqlite3", "~> 1.3.6"
require "sqlite3"
@@ -60,7 +60,7 @@ module ActiveRecord
include SQLite3::SchemaStatements
NATIVE_DATABASE_TYPES = {
- primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL",
+ primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL",
string: { name: "varchar" },
text: { name: "text" },
integer: { name: "integer" },
@@ -70,7 +70,8 @@ module ActiveRecord
time: { name: "time" },
date: { name: "date" },
binary: { name: "blob" },
- boolean: { name: "boolean" }
+ boolean: { name: "boolean" },
+ json: { name: "json" },
}
##
@@ -90,18 +91,17 @@ module ActiveRecord
# Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true
class_attribute :represent_boolean_as_integer, default: false
- class StatementPool < ConnectionAdapters::StatementPool
+ class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
private
-
def dealloc(stmt)
- stmt[:stmt].close unless stmt[:stmt].closed?
+ stmt.close unless stmt.closed?
end
end
def initialize(connection, logger, connection_options, config)
super(connection, logger, config)
- @active = nil
+ @active = true
@statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit]))
configure_connection
@@ -135,12 +135,16 @@ module ActiveRecord
true
end
+ def supports_json?
+ true
+ end
+
def supports_multi_insert?
sqlite_version >= "3.7.11"
end
def active?
- @active != false
+ @active
end
# Disconnects from the database if already connected. Otherwise, this
@@ -220,11 +224,8 @@ module ActiveRecord
stmt.close
end
else
- cache = @statements[sql] ||= {
- stmt: @connection.prepare(sql)
- }
- stmt = cache[:stmt]
- cols = cache[:cols] ||= stmt.columns
+ stmt = @statements[sql] ||= @connection.prepare(sql)
+ cols = stmt.columns
stmt.reset!
stmt.bind_params(type_casted_binds)
records = stmt.to_a
@@ -286,19 +287,18 @@ module ActiveRecord
rename_table_indexes(table_name, new_name)
end
- # See: https://www.sqlite.org/lang_altertable.html
- # SQLite has an additional restriction on the ALTER TABLE statement
- def valid_alter_table_type?(type)
- type.to_sym != :primary_key
+ def valid_alter_table_type?(type, options = {})
+ !invalid_alter_table_type?(type, options)
end
+ deprecate :valid_alter_table_type?
def add_column(table_name, column_name, type, options = {}) #:nodoc:
- if valid_alter_table_type?(type)
- super(table_name, column_name, type, options)
- else
+ if invalid_alter_table_type?(type, options)
alter_table(table_name) do |definition|
definition.column(column_name, type, options)
end
+ else
+ super
end
end
@@ -364,12 +364,30 @@ module ActiveRecord
end
def insert_fixtures(rows, table_name)
- rows.each do |row|
- insert_fixture(row, table_name)
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
+ `insert_fixtures` is deprecated and will be removed in the next version of Rails.
+ Consider using `insert_fixtures_set` for performance improvement.
+ MSG
+ insert_fixtures_set(table_name => rows)
+ end
+
+ def insert_fixtures_set(fixture_set, tables_to_delete = [])
+ disable_referential_integrity do
+ transaction(requires_new: true) do
+ tables_to_delete.each { |table| delete "DELETE FROM #{quote_table_name(table)}", "Fixture Delete" }
+
+ fixture_set.each do |table_name, rows|
+ rows.each { |row| insert_fixture(row, table_name) }
+ end
+ end
end
end
private
+ def initialize_type_map(m = type_map)
+ super
+ register_class_with_limit m, %r(int)i, SQLite3Integer
+ end
def table_structure(table_name)
structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA")
@@ -378,6 +396,12 @@ module ActiveRecord
end
alias column_definitions table_structure
+ # See: https://www.sqlite.org/lang_altertable.html
+ # SQLite has an additional restriction on the ALTER TABLE statement
+ def invalid_alter_table_type?(type, options)
+ type.to_sym == :primary_key || options[:primary_key]
+ end
+
def alter_table(table_name, options = {})
altered_table_name = "a#{table_name}"
caller = lambda { |definition| yield definition if block_given? }
@@ -399,18 +423,21 @@ module ActiveRecord
options[:id] = false
create_table(to, options) do |definition|
@definition = definition
- @definition.primary_key(from_primary_key) if from_primary_key.present?
+ if from_primary_key.is_a?(Array)
+ @definition.primary_keys from_primary_key
+ end
columns(from).each do |column|
column_name = options[:rename] ?
(options[:rename][column.name] ||
options[:rename][column.name.to_sym] ||
column.name) : column.name
- next if column_name == from_primary_key
@definition.column(column_name, column.type,
limit: column.limit, default: column.default,
precision: column.precision, scale: column.scale,
- null: column.null, collation: column.collation)
+ null: column.null, collation: column.collation,
+ primary_key: column_name == from_primary_key
+ )
end
yield @definition if block_given?
end
@@ -423,6 +450,9 @@ module ActiveRecord
def copy_table_indexes(from, to, rename = {})
indexes(from).each do |index|
name = index.name
+ # indexes sqlite creates for internal use start with `sqlite_` and
+ # don't need to be copied
+ next if name.starts_with?("sqlite_")
if to == "a#{from}"
name = "t#{name}"
elsif from == "a#{to}"
@@ -438,6 +468,7 @@ module ActiveRecord
# index name can't be the same
opts = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_"), internal: true }
opts[:unique] = true if index.unique
+ opts[:where] = index.where if index.where
add_index(to, columns, opts)
end
end
@@ -525,6 +556,17 @@ module ActiveRecord
def configure_connection
execute("PRAGMA foreign_keys = ON", "SCHEMA")
end
+
+ class SQLite3Integer < Type::Integer # :nodoc:
+ private
+ def _limit
+ # INTEGER storage class can be stored 8 bytes value.
+ # See https://www.sqlite.org/datatype3.html#storage_classes_and_datatypes
+ limit || 8
+ end
+ end
+
+ ActiveRecord::Type.register(:integer, SQLite3Integer, adapter: :sqlite3)
end
ActiveSupport.run_load_hooks(:active_record_sqlite3adapter, SQLite3Adapter)
end
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index 9a47edfba4..ee0e651912 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -57,6 +57,10 @@ module ActiveRecord
spec = resolver.resolve(config).symbolize_keys
spec[:name] = spec_name
+ # use the primary config if a config is not passed in and
+ # it's a three tier config
+ spec = spec[spec_name.to_sym] if spec[spec_name.to_sym]
+
connection_handler.establish_connection(spec)
end
@@ -140,6 +144,6 @@ module ActiveRecord
end
delegate :clear_active_connections!, :clear_reloadable_connections!,
- :clear_all_connections!, to: :connection_handler
+ :clear_all_connections!, :flush_idle_connections!, to: :connection_handler
end
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index 945c4eca78..e1a0b2ecf8 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -18,6 +18,13 @@ module ActiveRecord
mattr_accessor :logger, instance_writer: false
##
+ # :singleton-method:
+ #
+ # Specifies if the methods calling database queries should be logged below
+ # their relevant queries. Defaults to false.
+ mattr_accessor :verbose_query_logs, instance_writer: false, default: false
+
+ ##
# Contains the database configuration - as is typically stored in config/database.yml -
# as a Hash.
#
@@ -76,25 +83,13 @@ module ActiveRecord
# scope being ignored is error-worthy, rather than a warning.
mattr_accessor :error_on_ignored_order, instance_writer: false, default: false
- def self.error_on_ignored_order_or_limit
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- The flag error_on_ignored_order_or_limit is deprecated. Limits are
- now supported. Please use error_on_ignored_order instead.
- MSG
- error_on_ignored_order
- end
-
- def error_on_ignored_order_or_limit
- self.class.error_on_ignored_order_or_limit
- end
-
- def self.error_on_ignored_order_or_limit=(value)
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- The flag error_on_ignored_order_or_limit is deprecated. Limits are
- now supported. Please use error_on_ignored_order= instead.
- MSG
- self.error_on_ignored_order = value
- end
+ # :singleton-method:
+ # Specify the behavior for unsafe raw query methods. Values are as follows
+ # deprecated - Warnings are logged when unsafe raw SQL is passed to
+ # query methods.
+ # disabled - Unsafe raw SQL passed to query methods results in
+ # UnknownAttributeReference exception.
+ mattr_accessor :allow_unsafe_raw_sql, instance_writer: false, default: :deprecated
##
# :singleton-method:
@@ -286,10 +281,11 @@ module ActiveRecord
end
def relation
- relation = Relation.create(self, arel_table, predicate_builder)
+ relation = Relation.create(self)
if finder_needs_type_condition? && !ignore_default_scope?
- relation.where(type_condition).create_with(inheritance_column.to_s => sti_name)
+ relation.where!(type_condition)
+ relation.create_with!(inheritance_column.to_s => sti_name)
else
relation
end
@@ -386,8 +382,10 @@ module ActiveRecord
_run_initialize_callbacks
- @new_record = true
- @destroyed = false
+ @new_record = true
+ @destroyed = false
+ @_start_transaction_state = {}
+ @transaction_state = nil
super
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 5005d58f1c..ee4f818cbf 100644
--- a/activerecord/lib/active_record/counter_cache.rb
+++ b/activerecord/lib/active_record/counter_cache.rb
@@ -53,7 +53,7 @@ module ActiveRecord
unscoped.where(primary_key => object.id).update_all(updates)
end
- return true
+ true
end
# A generic "counter updater" implementation, intended primarily to be
diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb
new file mode 100644
index 0000000000..ffeed45030
--- /dev/null
+++ b/activerecord/lib/active_record/database_configurations.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module DatabaseConfigurations # :nodoc:
+ class DatabaseConfig
+ attr_reader :env_name, :spec_name, :config
+
+ def initialize(env_name, spec_name, config)
+ @env_name = env_name
+ @spec_name = spec_name
+ @config = config
+ end
+ end
+
+ # Selects the config for the specified environment and specification name
+ #
+ # For example if passed :development, and :animals it will select the database
+ # under the :development and :animals configuration level
+ def self.config_for_env_and_spec(environment, specification_name, configs = ActiveRecord::Base.configurations) # :nodoc:
+ configs_for(environment, configs).find do |db_config|
+ db_config.spec_name == specification_name
+ end
+ end
+
+ # Collects the configs for the environment passed in.
+ #
+ # If a block is given returns the specification name and configuration
+ # otherwise returns an array of DatabaseConfig structs for the environment.
+ def self.configs_for(env, configs = ActiveRecord::Base.configurations, &blk) # :nodoc:
+ env_with_configs = db_configs(configs).select do |db_config|
+ db_config.env_name == env
+ end
+
+ if block_given?
+ env_with_configs.each do |env_with_config|
+ yield env_with_config.spec_name, env_with_config.config
+ end
+ else
+ env_with_configs
+ end
+ end
+
+ # Given an env, spec and config creates DatabaseConfig structs with
+ # each attribute set.
+ def self.walk_configs(env_name, spec_name, config) # :nodoc:
+ if config["database"] || config["url"] || config["adapter"]
+ DatabaseConfig.new(env_name, spec_name, config)
+ else
+ config.each_pair.map do |sub_spec_name, sub_config|
+ walk_configs(env_name, sub_spec_name, sub_config)
+ end
+ end
+ end
+
+ # Walks all the configs passed in and returns an array
+ # of DatabaseConfig structs for each configuration.
+ def self.db_configs(configs = ActiveRecord::Base.configurations) # :nodoc:
+ configs.each_pair.flat_map do |env_name, config|
+ walk_configs(env_name, "primary", config)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb
index f373b98035..23ecb24542 100644
--- a/activerecord/lib/active_record/enum.rb
+++ b/activerecord/lib/active_record/enum.rb
@@ -141,10 +141,7 @@ module ActiveRecord
end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :name, :mapping, :subtype
end
@@ -221,6 +218,8 @@ module ActiveRecord
def detect_enum_conflict!(enum_name, method_name, klass_method = false)
if klass_method && dangerous_class_method?(method_name)
raise_conflict_error(enum_name, method_name, type: "class")
+ elsif klass_method && method_defined_within?(method_name, Relation)
+ raise_conflict_error(enum_name, method_name, type: "class", source: Relation.name)
elsif !klass_method && dangerous_attribute_method?(method_name)
raise_conflict_error(enum_name, method_name)
elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module)
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 9ef3316393..c2a180c939 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -120,13 +120,13 @@ module ActiveRecord
def initialize(adapter = nil, message: nil, table: nil, foreign_key: nil, target_table: nil, primary_key: nil)
@adapter = adapter
if table
- msg = <<-EOM.strip_heredoc
+ msg = +<<~EOM
Column `#{foreign_key}` on table `#{table}` has a type of `#{column_type(table, foreign_key)}`.
This does not match column `#{primary_key}` on `#{target_table}`, which has type `#{column_type(target_table, primary_key)}`.
To resolve this issue, change the type of the `#{foreign_key}` column on `#{table}` to be :integer. (For example `t.integer #{foreign_key}`).
EOM
else
- msg = <<-EOM
+ msg = +<<~EOM
There is a mismatch between the foreign key and primary key column types.
Verify that the foreign key column type and the primary key of the associated table match types.
EOM
@@ -335,8 +335,40 @@ module ActiveRecord
class IrreversibleOrderError < ActiveRecordError
end
- # TransactionTimeout will be raised when lock wait timeout expires.
- # Wait time value is set by innodb_lock_wait_timeout.
- class TransactionTimeout < StatementInvalid
+ # LockWaitTimeout will be raised when lock wait timeout exceeded.
+ class LockWaitTimeout < StatementInvalid
+ end
+
+ # StatementTimeout will be raised when statement timeout exceeded.
+ class StatementTimeout < StatementInvalid
+ end
+
+ # QueryCanceled will be raised when canceling statement due to user request.
+ class QueryCanceled < StatementInvalid
+ end
+
+ # UnknownAttributeReference is raised when an unknown and potentially unsafe
+ # value is passed to a query method when allow_unsafe_raw_sql is set to
+ # :disabled. For example, passing a non column name value to a relation's
+ # #order method might cause this exception.
+ #
+ # When working around this exception, caution should be taken to avoid SQL
+ # injection vulnerabilities when passing user-provided values to query
+ # methods. Known-safe values can be passed to query methods by wrapping them
+ # in Arel.sql.
+ #
+ # For example, with allow_unsafe_raw_sql set to :disabled, the following
+ # code would raise this exception:
+ #
+ # Post.order("length(title)").first
+ #
+ # The desired result can be accomplished by wrapping the known-safe string
+ # in Arel.sql:
+ #
+ # Post.order(Arel.sql("length(title)")).first
+ #
+ # Again, such a workaround should *not* be used when passing user-provided
+ # values, such as request parameters or model attributes to query methods.
+ class UnknownAttributeReference < ActiveRecordError
end
end
diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb
index 8bb54a24b7..7ccb938888 100644
--- a/activerecord/lib/active_record/explain.rb
+++ b/activerecord/lib/active_record/explain.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "explain_registry"
+require "active_record/explain_registry"
module ActiveRecord
module Explain
diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb
index 9252fa3fed..a86217abc0 100644
--- a/activerecord/lib/active_record/explain_subscriber.rb
+++ b/activerecord/lib/active_record/explain_subscriber.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
require "active_support/notifications"
-require_relative "explain_registry"
+require "active_record/explain_registry"
module ActiveRecord
class ExplainSubscriber # :nodoc:
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 12169fffa9..8f022ff7a7 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -6,8 +6,8 @@ require "zlib"
require "set"
require "active_support/dependencies"
require "active_support/core_ext/digest/uuid"
-require_relative "fixture_set/file"
-require_relative "errors"
+require "active_record/fixture_set/file"
+require "active_record/errors"
module ActiveRecord
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
@@ -169,13 +169,13 @@ module ActiveRecord
# self.use_transactional_tests = true
#
# test "godzilla" do
- # assert !Foo.all.empty?
+ # assert_not_empty Foo.all
# Foo.destroy_all
- # assert Foo.all.empty?
+ # assert_empty Foo.all
# end
#
# test "godzilla aftermath" do
- # assert !Foo.all.empty?
+ # assert_not_empty Foo.all
# end
# end
#
@@ -540,47 +540,38 @@ module ActiveRecord
}
unless files_to_read.empty?
- connection.disable_referential_integrity do
- fixtures_map = {}
-
- fixture_sets = files_to_read.map do |fs_name|
- klass = class_names[fs_name]
- conn = klass ? klass.connection : connection
- fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new
- conn,
- fs_name,
- klass,
- ::File.join(fixtures_directory, fs_name))
- end
-
- update_all_loaded_fixtures fixtures_map
-
- connection.transaction(requires_new: true) do
- deleted_tables = Hash.new { |h, k| h[k] = Set.new }
- fixture_sets.each do |fs|
- conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection
- table_rows = fs.table_rows
+ fixtures_map = {}
+
+ fixture_sets = files_to_read.map do |fs_name|
+ klass = class_names[fs_name]
+ conn = klass ? klass.connection : connection
+ fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new
+ conn,
+ fs_name,
+ klass,
+ ::File.join(fixtures_directory, fs_name))
+ end
- table_rows.each_key do |table|
- unless deleted_tables[conn].include? table
- conn.delete "DELETE FROM #{conn.quote_table_name(table)}", "Fixture Delete"
- end
- deleted_tables[conn] << table
- end
+ update_all_loaded_fixtures fixtures_map
+ fixture_sets_by_connection = fixture_sets.group_by { |fs| fs.model_class ? fs.model_class.connection : connection }
- table_rows.each do |fixture_set_name, rows|
- conn.insert_fixtures(rows, fixture_set_name)
- end
+ fixture_sets_by_connection.each do |conn, set|
+ table_rows_for_connection = Hash.new { |h, k| h[k] = [] }
- # Cap primary key sequences to max(pk).
- if conn.respond_to?(:reset_pk_sequence!)
- conn.reset_pk_sequence!(fs.table_name)
- end
+ set.each do |fs|
+ fs.table_rows.each do |table, rows|
+ table_rows_for_connection[table].unshift(*rows)
end
end
+ conn.insert_fixtures_set(table_rows_for_connection, table_rows_for_connection.keys)
- cache_fixtures(connection, fixtures_map)
+ # Cap primary key sequences to max(pk).
+ if conn.respond_to?(:reset_pk_sequence!)
+ set.each { |fs| conn.reset_pk_sequence!(fs.table_name) }
+ end
end
+
+ cache_fixtures(connection, fixtures_map)
end
cached_fixtures(connection, fixture_set_names)
end
@@ -883,6 +874,7 @@ module ActiveRecord
class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances
class_attribute :pre_loaded_fixtures, default: false
class_attribute :config, default: ActiveRecord::Base
+ class_attribute :lock_threads, default: true
end
module ClassMethods
@@ -982,7 +974,7 @@ module ActiveRecord
@fixture_connections = enlist_fixture_connections
@fixture_connections.each do |connection|
connection.begin_transaction joinable: false
- connection.pool.lock_thread = true
+ connection.pool.lock_thread = true if lock_threads
end
# When connections are established in the future, begin a transaction too
@@ -998,7 +990,7 @@ module ActiveRecord
if connection && !@fixture_connections.include?(connection)
connection.begin_transaction joinable: false
- connection.pool.lock_thread = true
+ connection.pool.lock_thread = true if lock_threads
@fixture_connections << connection
end
end
diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb
index 7ccb57b305..72035a986b 100644
--- a/activerecord/lib/active_record/gem_version.rb
+++ b/activerecord/lib/active_record/gem_version.rb
@@ -7,8 +7,8 @@ module ActiveRecord
end
module VERSION
- MAJOR = 5
- MINOR = 2
+ MAJOR = 6
+ MINOR = 0
TINY = 0
PRE = "alpha"
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 0715c11473..6891c575c7 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -47,22 +47,21 @@ module ActiveRecord
# Determines if one of the attributes passed in is the inheritance column,
# and if the inheritance column is attr accessible, it initializes an
# instance of the given subclass instead of the base class.
- def new(*args, &block)
+ def new(attributes = nil, &block)
if abstract_class? || self == Base
raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated."
end
- attrs = args.first
if has_attribute?(inheritance_column)
- subclass = subclass_from_attributes(attrs)
+ subclass = subclass_from_attributes(attributes)
- if subclass.nil? && base_class == self
+ if subclass.nil? && base_class?
subclass = subclass_from_attributes(column_defaults)
end
end
if subclass && subclass != self
- subclass.new(*args, &block)
+ subclass.new(attributes, &block)
else
super
end
@@ -105,21 +104,53 @@ module ActiveRecord
end
end
- # Set this to true if this is an abstract class (see <tt>abstract_class?</tt>).
- # If you are using inheritance with ActiveRecord and don't want child classes
- # to utilize the implied STI table name of the parent class, this will need to be true.
- # For example, given the following:
+ # Returns whether the class is a base class.
+ # See #base_class for more information.
+ def base_class?
+ base_class == self
+ end
+
+ # Set this to +true+ if this is an abstract class (see
+ # <tt>abstract_class?</tt>).
+ # If you are using inheritance with Active Record and don't want a class
+ # to be considered as part of the STI hierarchy, you must set this to
+ # true.
+ # +ApplicationRecord+, for example, is generated as an abstract class.
+ #
+ # Consider the following default behaviour:
+ #
+ # Shape = Class.new(ActiveRecord::Base)
+ # Polygon = Class.new(Shape)
+ # Square = Class.new(Polygon)
+ #
+ # Shape.table_name # => "shapes"
+ # Polygon.table_name # => "shapes"
+ # Square.table_name # => "shapes"
+ # Shape.create! # => #<Shape id: 1, type: nil>
+ # Polygon.create! # => #<Polygon id: 2, type: "Polygon">
+ # Square.create! # => #<Square id: 3, type: "Square">
#
- # class SuperClass < ActiveRecord::Base
+ # However, when using <tt>abstract_class</tt>, +Shape+ is omitted from
+ # the hierarchy:
+ #
+ # class Shape < ActiveRecord::Base
# self.abstract_class = true
# end
- # class Child < SuperClass
- # self.table_name = 'the_table_i_really_want'
- # end
- #
+ # Polygon = Class.new(Shape)
+ # Square = Class.new(Polygon)
#
- # <tt>self.abstract_class = true</tt> is required to make <tt>Child<.find,.create, or any Arel method></tt> use <tt>the_table_i_really_want</tt> instead of a table called <tt>super_classes</tt>
+ # Shape.table_name # => nil
+ # Polygon.table_name # => "polygons"
+ # Square.table_name # => "polygons"
+ # Shape.create! # => NotImplementedError: Shape is an abstract class and cannot be instantiated.
+ # Polygon.create! # => #<Polygon id: 1, type: nil>
+ # Square.create! # => #<Square id: 2, type: "Square">
#
+ # Note that in the above example, to disallow the creation of a plain
+ # +Polygon+, you should use <tt>validates :type, presence: true</tt>,
+ # instead of setting it as an abstract class. This way, +Polygon+ will
+ # stay in the hierarchy, and Active Record will continue to correctly
+ # derive the table name.
attr_accessor :abstract_class
# Returns whether this class is an abstract class or not.
@@ -131,6 +162,10 @@ module ActiveRecord
store_full_sti_class ? name : name.demodulize
end
+ def polymorphic_name
+ base_class.name
+ end
+
def inherited(subclass)
subclass.instance_variable_set(:@_type_candidates_cache, Concurrent::Map.new)
super
diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb
index 14795cc815..3626a13d7c 100644
--- a/activerecord/lib/active_record/internal_metadata.rb
+++ b/activerecord/lib/active_record/internal_metadata.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "scoping/default"
-require_relative "scoping/named"
+require "active_record/scoping/default"
+require "active_record/scoping/named"
module ActiveRecord
# This class is used to create a table that keeps track of values and keys such
@@ -17,7 +17,7 @@ module ActiveRecord
end
def []=(key, value)
- find_or_initialize_by(key: key).update_attributes!(value: value)
+ find_or_initialize_by(key: key).update!(value: value)
end
def [](key)
diff --git a/activerecord/lib/active_record/legacy_yaml_adapter.rb b/activerecord/lib/active_record/legacy_yaml_adapter.rb
index 23644aab8f..ffa095dd94 100644
--- a/activerecord/lib/active_record/legacy_yaml_adapter.rb
+++ b/activerecord/lib/active_record/legacy_yaml_adapter.rb
@@ -8,7 +8,7 @@ module ActiveRecord
case coder["active_record_yaml_version"]
when 1, 2 then coder
else
- if coder["attributes"].is_a?(AttributeSet)
+ if coder["attributes"].is_a?(ActiveModel::AttributeSet)
Rails420.convert(klass, coder)
else
Rails41.convert(klass, coder)
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index e1e24e2814..7f096bb532 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -61,13 +61,6 @@ module ActiveRecord
end
private
-
- def increment_lock
- lock_col = self.class.locking_column
- previous_lock_value = send(lock_col)
- send("#{lock_col}=", previous_lock_value + 1)
- end
-
def _create_record(attribute_names = self.attribute_names, *)
if locking_enabled?
# We always want to persist the locking version, even if we don't detect
@@ -77,63 +70,56 @@ module ActiveRecord
super
end
- def _update_record(attribute_names = self.attribute_names)
+ def _touch_row(attribute_names, time)
+ super
+ ensure
+ clear_attribute_change(self.class.locking_column) if locking_enabled?
+ end
+
+ def _update_row(attribute_names, attempted_action = "update")
return super unless locking_enabled?
- return 0 if attribute_names.empty?
begin
- lock_col = self.class.locking_column
-
- previous_lock_value = read_attribute_before_type_cast(lock_col)
-
- increment_lock
-
- attribute_names.push(lock_col)
+ locking_column = self.class.locking_column
+ previous_lock_value = read_attribute_before_type_cast(locking_column)
+ attribute_names << locking_column
- relation = self.class.unscoped
+ self[locking_column] += 1
- affected_rows = relation.where(
- self.class.primary_key => id,
- lock_col => previous_lock_value
- ).update_all(
- attributes_for_update(attribute_names).map do |name|
- [name, _read_attribute(name)]
- end.to_h
+ affected_rows = self.class._update_record(
+ attributes_with_values(attribute_names),
+ self.class.primary_key => id_in_database,
+ locking_column => previous_lock_value
)
- unless affected_rows == 1
- raise ActiveRecord::StaleObjectError.new(self, "update")
+ if affected_rows != 1
+ raise ActiveRecord::StaleObjectError.new(self, attempted_action)
end
affected_rows
# If something went wrong, revert the locking_column value.
rescue Exception
- send("#{lock_col}=", previous_lock_value.to_i)
-
+ self[locking_column] = previous_lock_value.to_i
raise
end
end
def destroy_row
- affected_rows = super
-
- if locking_enabled? && affected_rows != 1
- raise ActiveRecord::StaleObjectError.new(self, "destroy")
- end
+ return super unless locking_enabled?
- affected_rows
- end
+ locking_column = self.class.locking_column
- def relation_for_destroy
- relation = super
+ affected_rows = self.class._delete_record(
+ self.class.primary_key => id_in_database,
+ locking_column => read_attribute_before_type_cast(locking_column)
+ )
- if locking_enabled?
- locking_column = self.class.locking_column
- relation = relation.where(locking_column => read_attribute_before_type_cast(locking_column))
+ if affected_rows != 1
+ raise ActiveRecord::StaleObjectError.new(self, "destroy")
end
- relation
+ affected_rows
end
module ClassMethods
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index 72bccd4906..5d1d15c94d 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -62,13 +62,14 @@ module ActiveRecord
# the locked record.
def lock!(lock = true)
if persisted?
- if changed?
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Locking a record with unpersisted changes is deprecated and will raise an
- exception in Rails 5.2. Use `save` to persist the changes, or `reload` to
- discard them explicitly.
+ if has_changes_to_save?
+ raise(<<-MSG.squish)
+ Locking a record with unpersisted changes is not supported. Use
+ `save` to persist the changes, or `reload` to discard them
+ explicitly.
MSG
end
+
reload(lock: lock)
end
self
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index 405f3a30c6..013c3765b2 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -90,6 +90,47 @@ module ActiveRecord
def logger
ActiveRecord::Base.logger
end
+
+ def debug(progname = nil, &block)
+ return unless super
+
+ if ActiveRecord::Base.verbose_query_logs
+ log_query_source
+ end
+ end
+
+ def log_query_source
+ source_line, line_number = extract_callstack(caller_locations)
+
+ if source_line
+ if defined?(::Rails.root)
+ app_root = "#{::Rails.root.to_s}/".freeze
+ source_line = source_line.sub(app_root, "")
+ end
+
+ logger.debug(" ↳ #{ source_line }:#{ line_number }")
+ end
+ end
+
+ def extract_callstack(callstack)
+ line = callstack.find do |frame|
+ frame.absolute_path && !ignored_callstack(frame.absolute_path)
+ end
+
+ offending_line = line || callstack.first
+
+ [
+ offending_line.path,
+ offending_line.lineno
+ ]
+ end
+
+ RAILS_GEM_ROOT = File.expand_path("../../..", __dir__) + "/"
+
+ def ignored_callstack(path)
+ path.start_with?(RAILS_GEM_ROOT) ||
+ path.start_with?(RbConfig::CONFIG["rubylibdir"])
+ end
end
end
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 8845e26ab7..025201c20b 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -1,5 +1,6 @@
# frozen_string_literal: true
+require "benchmark"
require "set"
require "zlib"
require "active_support/core_ext/module/attribute_accessors"
@@ -140,6 +141,7 @@ module ActiveRecord
class ConcurrentMigrationError < MigrationError #:nodoc:
DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running.".freeze
+ RELEASE_LOCK_FAILED_MESSAGE = "Failed to release advisory lock".freeze
def initialize(message = DEFAULT_MESSAGE)
super
@@ -550,7 +552,7 @@ module ActiveRecord
end
def call(env)
- mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
+ mtime = ActiveRecord::Base.connection.migration_context.last_migration.mtime.to_i
if @last_check < mtime
ActiveRecord::Migration.check_pending!(connection)
@last_check = mtime
@@ -575,13 +577,14 @@ module ActiveRecord
# Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending.
def check_pending!(connection = Base.connection)
- raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection)
+ raise ActiveRecord::PendingMigrationError if connection.migration_context.needs_migration?
end
def load_schema_if_pending!
- if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations?
+ if Base.connection.migration_context.needs_migration? || !Base.connection.migration_context.any_migrations?
# Roundtrip to Rake to allow plugins to hook into database initialization.
- FileUtils.cd Rails.root do
+ root = defined?(ENGINE_ROOT) ? ENGINE_ROOT : Rails.root
+ FileUtils.cd(root) do
current_config = Base.connection_config
Base.clear_all_connections!
system("bin/rails db:test:prepare")
@@ -733,6 +736,24 @@ module ActiveRecord
execute_block { yield helper }
end
+ # Used to specify an operation that is only run when migrating up
+ # (for example, populating a new column with its initial values).
+ #
+ # In the following example, the new column +published+ will be given
+ # the value +true+ for all existing records.
+ #
+ # class AddPublishedToPosts < ActiveRecord::Migration[5.2]
+ # def change
+ # add_column :posts, :published, :boolean, default: false
+ # up_only do
+ # execute "update posts set published = 'true'"
+ # end
+ # end
+ # end
+ def up_only
+ execute_block { yield } unless reverting?
+ end
+
# Runs the given migration classes.
# Last argument can specify options:
# - :direction (default is :up)
@@ -857,10 +878,10 @@ module ActiveRecord
FileUtils.mkdir_p(destination) unless File.exist?(destination)
- destination_migrations = ActiveRecord::Migrator.migrations(destination)
+ destination_migrations = ActiveRecord::MigrationContext.new(destination).migrations
last = destination_migrations.last
sources.each do |scope, path|
- source_migrations = ActiveRecord::Migrator.migrations(path)
+ source_migrations = ActiveRecord::MigrationContext.new(path).migrations
source_migrations.each do |migration|
source = File.binread(migration.filename)
@@ -978,136 +999,147 @@ module ActiveRecord
end
end
- class Migrator#:nodoc:
- class << self
- attr_writer :migrations_paths
- alias :migrations_path= :migrations_paths=
-
- def migrate(migrations_paths, target_version = nil, &block)
- case
- when target_version.nil?
- up(migrations_paths, target_version, &block)
- when current_version == 0 && target_version == 0
- []
- when current_version > target_version
- down(migrations_paths, target_version, &block)
- else
- up(migrations_paths, target_version, &block)
- end
- end
+ class MigrationContext # :nodoc:
+ attr_reader :migrations_paths
- def rollback(migrations_paths, steps = 1)
- move(:down, migrations_paths, steps)
- end
+ def initialize(migrations_paths)
+ @migrations_paths = migrations_paths
+ end
- def forward(migrations_paths, steps = 1)
- move(:up, migrations_paths, steps)
+ def migrate(target_version = nil, &block)
+ case
+ when target_version.nil?
+ up(target_version, &block)
+ when current_version == 0 && target_version == 0
+ []
+ when current_version > target_version
+ down(target_version, &block)
+ else
+ up(target_version, &block)
end
+ end
+
+ def rollback(steps = 1)
+ move(:down, steps)
+ end
- def up(migrations_paths, target_version = nil)
- migrations = migrations(migrations_paths)
- migrations.select! { |m| yield m } if block_given?
+ def forward(steps = 1)
+ move(:up, steps)
+ end
- new(:up, migrations, target_version).migrate
+ def up(target_version = nil)
+ selected_migrations = if block_given?
+ migrations.select { |m| yield m }
+ else
+ migrations
end
- def down(migrations_paths, target_version = nil)
- migrations = migrations(migrations_paths)
- migrations.select! { |m| yield m } if block_given?
+ Migrator.new(:up, selected_migrations, target_version).migrate
+ end
- new(:down, migrations, target_version).migrate
+ def down(target_version = nil)
+ selected_migrations = if block_given?
+ migrations.select { |m| yield m }
+ else
+ migrations
end
- def run(direction, migrations_paths, target_version)
- new(direction, migrations(migrations_paths), target_version).run
- end
+ Migrator.new(:down, selected_migrations, target_version).migrate
+ end
- def open(migrations_paths)
- new(:up, migrations(migrations_paths), nil)
- end
+ def run(direction, target_version)
+ Migrator.new(direction, migrations, target_version).run
+ end
- def schema_migrations_table_name
- SchemaMigration.table_name
- end
- deprecate :schema_migrations_table_name
+ def open
+ Migrator.new(:up, migrations, nil)
+ end
- def get_all_versions(connection = Base.connection)
- if SchemaMigration.table_exists?
- SchemaMigration.all_versions.map(&:to_i)
- else
- []
- end
+ def get_all_versions
+ if SchemaMigration.table_exists?
+ SchemaMigration.all_versions.map(&:to_i)
+ else
+ []
end
+ end
- def current_version(connection = Base.connection)
- get_all_versions(connection).max || 0
- end
+ def current_version
+ get_all_versions.max || 0
+ rescue ActiveRecord::NoDatabaseError
+ end
- def needs_migration?(connection = Base.connection)
- (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0
- end
+ def needs_migration?
+ (migrations.collect(&:version) - get_all_versions).size > 0
+ end
- def any_migrations?
- migrations(migrations_paths).any?
- end
+ def any_migrations?
+ migrations.any?
+ end
- def last_migration #:nodoc:
- migrations(migrations_paths).last || NullMigration.new
- end
+ def last_migration #:nodoc:
+ migrations.last || NullMigration.new
+ end
- def migrations_paths
- @migrations_paths ||= ["db/migrate"]
- # just to not break things if someone uses: migrations_path = some_string
- Array(@migrations_paths)
- end
+ def parse_migration_filename(filename) # :nodoc:
+ File.basename(filename).scan(Migration::MigrationFilenameRegexp).first
+ end
+
+ def migrations
+ migrations = migration_files.map do |file|
+ version, name, scope = parse_migration_filename(file)
+ raise IllegalMigrationNameError.new(file) unless version
+ version = version.to_i
+ name = name.camelize
- def parse_migration_filename(filename) # :nodoc:
- File.basename(filename).scan(Migration::MigrationFilenameRegexp).first
+ MigrationProxy.new(name, version, file, scope)
end
- def migrations(paths)
- paths = Array(paths)
+ migrations.sort_by(&:version)
+ end
- migrations = migration_files(paths).map do |file|
- version, name, scope = parse_migration_filename(file)
- raise IllegalMigrationNameError.new(file) unless version
- version = version.to_i
- name = name.camelize
+ def migrations_status
+ db_list = ActiveRecord::SchemaMigration.normalized_versions
- MigrationProxy.new(name, version, file, scope)
- end
+ file_list = migration_files.map do |file|
+ version, name, scope = parse_migration_filename(file)
+ raise IllegalMigrationNameError.new(file) unless version
+ version = ActiveRecord::SchemaMigration.normalize_migration_number(version)
+ status = db_list.delete(version) ? "up" : "down"
+ [status, version, (name + scope).humanize]
+ end.compact
- migrations.sort_by(&:version)
+ db_list.map! do |version|
+ ["up", version, "********** NO FILE **********"]
end
- def migrations_status(paths)
- paths = Array(paths)
-
- db_list = ActiveRecord::SchemaMigration.normalized_versions
+ (db_list + file_list).sort_by { |_, version, _| version }
+ end
- file_list = migration_files(paths).map do |file|
- version, name, scope = parse_migration_filename(file)
- raise IllegalMigrationNameError.new(file) unless version
- version = ActiveRecord::SchemaMigration.normalize_migration_number(version)
- status = db_list.delete(version) ? "up" : "down"
- [status, version, (name + scope).humanize]
- end.compact
+ def migration_files
+ paths = Array(migrations_paths)
+ Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
+ end
- db_list.map! do |version|
- ["up", version, "********** NO FILE **********"]
- end
+ def current_environment
+ ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
+ end
- (db_list + file_list).sort_by { |_, version, _| version }
- end
+ def protected_environment?
+ ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment
+ end
- def migration_files(paths)
- Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }]
- end
+ def last_stored_environment
+ return nil if current_version == 0
+ raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists?
- private
+ environment = ActiveRecord::InternalMetadata[:environment]
+ raise NoEnvironmentInSchemaError unless environment
+ environment
+ end
- def move(direction, migrations_paths, steps)
- migrator = new(direction, migrations(migrations_paths))
+ private
+ def move(direction, steps)
+ migrator = Migrator.new(direction, migrations)
if current_version != 0 && !migrator.current_migration
raise UnknownMigrationVersionError.new(current_version)
@@ -1122,10 +1154,29 @@ module ActiveRecord
finish = migrator.migrations[start_index + steps]
version = finish ? finish.version : 0
- send(direction, migrations_paths, version)
+ send(direction, version)
+ end
+ end
+
+ class Migrator # :nodoc:
+ class << self
+ attr_accessor :migrations_paths
+
+ def migrations_path=(path)
+ ActiveSupport::Deprecation.warn \
+ "ActiveRecord::Migrator.migrations_paths= is now deprecated and will be removed in Rails 6.0." \
+ "You can set the `migrations_paths` on the `connection` instead through the `database.yml`."
+ self.migrations_paths = [path]
+ end
+
+ # For cases where a table doesn't exist like loading from schema cache
+ def current_version
+ MigrationContext.new(migrations_paths).current_version
end
end
+ self.migrations_paths = ["db/migrate"]
+
def initialize(direction, migrations, target_version = nil)
@direction = direction
@target_version = target_version
@@ -1188,7 +1239,7 @@ module ActiveRecord
end
def load_migrated
- @migrated_versions = Set.new(self.class.get_all_versions)
+ @migrated_versions = Set.new(Base.connection.migration_context.get_all_versions)
end
private
@@ -1220,7 +1271,7 @@ module ActiveRecord
# Stores the current environment in the database.
def record_environment
return if down?
- ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
+ ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment
end
def ran?(migration)
@@ -1229,7 +1280,7 @@ module ActiveRecord
# Return true if a valid version is not provided.
def invalid_target?
- !target && @target_version && @target_version > 0
+ @target_version && @target_version != 0 && !target
end
def execute_migration_in_transaction(migration, direction)
@@ -1262,10 +1313,10 @@ module ActiveRecord
end
def validate(migrations)
- name , = migrations.group_by(&:name).find { |_, v| v.length > 1 }
+ name, = migrations.group_by(&:name).find { |_, v| v.length > 1 }
raise DuplicateMigrationNameError.new(name) if name
- version , = migrations.group_by(&:version).find { |_, v| v.length > 1 }
+ version, = migrations.group_by(&:version).find { |_, v| v.length > 1 }
raise DuplicateMigrationVersionError.new(version) if version
end
@@ -1279,23 +1330,6 @@ module ActiveRecord
end
end
- def self.last_stored_environment
- return nil if current_version == 0
- raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists?
-
- environment = ActiveRecord::InternalMetadata[:environment]
- raise NoEnvironmentInSchemaError unless environment
- environment
- end
-
- def self.current_environment
- ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
- end
-
- def self.protected_environment?
- ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment
- end
-
def up?
@direction == :up
end
@@ -1323,12 +1357,17 @@ module ActiveRecord
def with_advisory_lock
lock_id = generate_migrator_advisory_lock_id
- got_lock = Base.connection.get_advisory_lock(lock_id)
+ connection = Base.connection
+ got_lock = connection.get_advisory_lock(lock_id)
raise ConcurrentMigrationError unless got_lock
load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock
yield
ensure
- Base.connection.release_advisory_lock(lock_id) if got_lock
+ if got_lock && !connection.release_advisory_lock(lock_id)
+ raise ConcurrentMigrationError.new(
+ ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE
+ )
+ end
end
MIGRATOR_SALT = 2053462845
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index a3a5e0fa16..087632b10f 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -85,7 +85,7 @@ module ActiveRecord
# invert the +command+.
def inverse_of(command, args, &block)
method = :"invert_#{command}"
- raise IrreversibleMigration, <<-MSG.strip_heredoc unless respond_to?(method, true)
+ raise IrreversibleMigration, <<~MSG unless respond_to?(method, true)
This migration uses #{command}, which is not automatically reversible.
To make the migration reversible you can either:
1. Define #up and #down methods in place of the #change method.
@@ -110,7 +110,7 @@ module ActiveRecord
private
- module StraightReversions
+ module StraightReversions # :nodoc:
private
{ transaction: :transaction,
execute_block: :execute_block,
@@ -161,8 +161,8 @@ module ActiveRecord
table, columns, options = *args
options ||= {}
- index_name = options[:name]
- options_hash = index_name ? { name: index_name } : { column: columns }
+ options_hash = options.slice(:name, :algorithm)
+ options_hash[:column] = columns if !options_hash[:name]
[:remove_index, [table, options_hash]]
end
diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb
index 784292f3f9..0edaaa0cf9 100644
--- a/activerecord/lib/active_record/migration/compatibility.rb
+++ b/activerecord/lib/active_record/migration/compatibility.rb
@@ -13,13 +13,41 @@ module ActiveRecord
const_get(name)
end
- V5_2 = Current
+ V6_0 = Current
+
+ class V5_2 < V6_0
+ end
class V5_1 < V5_2
+ def change_column(table_name, column_name, type, options = {})
+ if adapter_name == "PostgreSQL"
+ clear_cache!
+ sql = connection.send(:change_column_sql, table_name, column_name, type, options)
+ execute "ALTER TABLE #{quote_table_name(table_name)} #{sql}"
+ change_column_default(table_name, column_name, options[:default]) if options.key?(:default)
+ change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
+ change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment)
+ else
+ super
+ end
+ end
+
+ def create_table(table_name, options = {})
+ if adapter_name == "Mysql2"
+ super(table_name, options: "ENGINE=InnoDB", **options)
+ else
+ super
+ end
+ end
end
class V5_0 < V5_1
module TableDefinition
+ def primary_key(name, type = :primary_key, **options)
+ type = :integer if type == :primary_key
+ super
+ end
+
def references(*args, **options)
super(*args, type: :integer, **options)
end
@@ -47,11 +75,8 @@ module ActiveRecord
end
if block_given?
- super(table_name, options) do |t|
- class << t
- prepend TableDefinition
- end
- yield t
+ super do |t|
+ yield compatible_table_definition(t)
end
else
super
@@ -60,21 +85,46 @@ module ActiveRecord
def change_table(table_name, options = {})
if block_given?
- super(table_name, options) do |t|
- class << t
- prepend TableDefinition
- end
- yield t
+ super do |t|
+ yield compatible_table_definition(t)
end
else
super
end
end
+ def create_join_table(table_1, table_2, column_options: {}, **options)
+ column_options.reverse_merge!(type: :integer)
+
+ if block_given?
+ super do |t|
+ yield compatible_table_definition(t)
+ end
+ else
+ super
+ end
+ end
+
+ def add_column(table_name, column_name, type, options = {})
+ if type == :primary_key
+ type = :integer
+ options[:primary_key] = true
+ end
+ super
+ end
+
def add_reference(table_name, ref_name, **options)
super(table_name, ref_name, type: :integer, **options)
end
alias :add_belongs_to :add_reference
+
+ private
+ def compatible_table_definition(t)
+ class << t
+ prepend TableDefinition
+ end
+ t
+ end
end
class V4_2 < V5_0
@@ -93,11 +143,8 @@ module ActiveRecord
def create_table(table_name, options = {})
if block_given?
- super(table_name, options) do |t|
- class << t
- prepend TableDefinition
- end
- yield t
+ super do |t|
+ yield compatible_table_definition(t)
end
else
super
@@ -106,11 +153,8 @@ module ActiveRecord
def change_table(table_name, options = {})
if block_given?
- super(table_name, options) do |t|
- class << t
- prepend TableDefinition
- end
- yield t
+ super do |t|
+ yield compatible_table_definition(t)
end
else
super
@@ -146,6 +190,12 @@ module ActiveRecord
end
private
+ def compatible_table_definition(t)
+ class << t
+ prepend TableDefinition
+ end
+ super
+ end
def index_name_for_remove(table_name, options = {})
index_name = index_name(table_name, options)
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 34c0ef4e75..694ff85fa1 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -87,19 +87,6 @@ module ActiveRecord
# Sets the name of the internal metadata table.
##
- # :singleton-method: protected_environments
- # :call-seq: protected_environments
- #
- # The array of names of environments where destructive actions should be prohibited. By default,
- # the value is <tt>["production"]</tt>.
-
- ##
- # :singleton-method: protected_environments=
- # :call-seq: protected_environments=(environments)
- #
- # Sets an array of names of environments where destructive actions should be prohibited.
-
- ##
# :singleton-method: pluralize_table_names
# :call-seq: pluralize_table_names
#
@@ -115,20 +102,6 @@ module ActiveRecord
# If true, the default table name for a Product class will be "products". If false, it would just be "product".
# See table_name for the full rules on table/class naming. This is true, by default.
- ##
- # :singleton-method: ignored_columns
- # :call-seq: ignored_columns
- #
- # The list of columns names the model should ignore. Ignored columns won't have attribute
- # accessors defined, and won't be referenced in SQL queries.
-
- ##
- # :singleton-method: ignored_columns=
- # :call-seq: ignored_columns=(columns)
- #
- # Sets the columns names the model should ignore. Ignored columns won't have attribute
- # accessors defined, and won't be referenced in SQL queries.
-
included do
mattr_accessor :primary_key_prefix_type, instance_writer: false
@@ -136,11 +109,11 @@ module ActiveRecord
class_attribute :table_name_suffix, instance_writer: false, default: ""
class_attribute :schema_migrations_table_name, instance_accessor: false, default: "schema_migrations"
class_attribute :internal_metadata_table_name, instance_accessor: false, default: "ar_internal_metadata"
- class_attribute :protected_environments, instance_accessor: false, default: [ "production" ]
class_attribute :pluralize_table_names, instance_writer: false, default: true
- class_attribute :ignored_columns, instance_accessor: false, default: [].freeze
+ self.protected_environments = ["production"]
self.inheritance_column = "type"
+ self.ignored_columns = [].freeze
delegate :type_for_attribute, to: :class
@@ -252,6 +225,21 @@ module ActiveRecord
(parents.detect { |p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix
end
+ # The array of names of environments where destructive actions should be prohibited. By default,
+ # the value is <tt>["production"]</tt>.
+ def protected_environments
+ if defined?(@protected_environments)
+ @protected_environments
+ else
+ superclass.protected_environments
+ end
+ end
+
+ # Sets an array of names of environments where destructive actions should be prohibited.
+ def protected_environments=(environments)
+ @protected_environments = environments.map(&:to_s)
+ end
+
# Defines the name of the table column which will store the class name on single-table
# inheritance situations.
#
@@ -271,8 +259,24 @@ module ActiveRecord
@explicit_inheritance_column = true
end
+ # The list of columns names the model should ignore. Ignored columns won't have attribute
+ # accessors defined, and won't be referenced in SQL queries.
+ def ignored_columns
+ if defined?(@ignored_columns)
+ @ignored_columns
+ else
+ superclass.ignored_columns
+ end
+ end
+
+ # Sets the columns names the model should ignore. Ignored columns won't have attribute
+ # accessors defined, and won't be referenced in SQL queries.
+ def ignored_columns=(columns)
+ @ignored_columns = columns.map(&:to_s)
+ end
+
def sequence_name
- if base_class == self
+ if base_class?
@sequence_name ||= reset_sequence_name
else
(@sequence_name ||= nil) || base_class.sequence_name
@@ -321,11 +325,11 @@ module ActiveRecord
end
def attributes_builder # :nodoc:
- @attributes_builder ||= AttributeSet::Builder.new(attribute_types, primary_key) do |name|
- unless columns_hash.key?(name)
- _default_attributes[name].dup
- end
+ unless defined?(@attributes_builder) && @attributes_builder
+ defaults = _default_attributes.except(*(column_names - [primary_key]))
+ @attributes_builder = ActiveModel::AttributeSet::Builder.new(attribute_types, defaults)
end
+ @attributes_builder
end
def columns_hash # :nodoc:
@@ -344,7 +348,7 @@ module ActiveRecord
end
def yaml_encoder # :nodoc:
- @yaml_encoder ||= AttributeSet::YAMLEncoder.new(attribute_types)
+ @yaml_encoder ||= ActiveModel::AttributeSet::YAMLEncoder.new(attribute_types)
end
# Returns the type of the attribute with the given name, after applying
@@ -357,8 +361,9 @@ module ActiveRecord
# it).
#
# +attr_name+ The name of the attribute to retrieve the type for. Must be
- # a string
+ # a string or a symbol.
def type_for_attribute(attr_name, &block)
+ attr_name = attr_name.to_s
if block
attribute_types.fetch(attr_name, &block)
else
@@ -374,7 +379,8 @@ module ActiveRecord
end
def _default_attributes # :nodoc:
- @default_attributes ||= AttributeSet.new({})
+ load_schema
+ @default_attributes ||= ActiveModel::AttributeSet.new({})
end
# Returns an array of column names as strings.
@@ -421,7 +427,7 @@ module ActiveRecord
# end
def reset_column_information
connection.clear_cache!
- undefine_attribute_methods
+ ([self] + descendants).each(&:undefine_attribute_methods)
connection.schema_cache.clear_data_source_cache!(table_name)
reload_schema_from_cache
@@ -495,8 +501,7 @@ module ActiveRecord
# Computes and returns a table name according to default conventions.
def compute_table_name
- base = base_class
- if self == base
+ if base_class?
# Nested classes are prefixed with singular parent table name.
if parent < Base && !parent.abstract_class?
contained = parent.table_name
@@ -507,7 +512,7 @@ module ActiveRecord
"#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}"
else
# STI subclasses always use their superclass' table.
- base.table_name
+ base_class.table_name
end
end
end
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 435c81c153..fa20bce3a9 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -63,6 +63,18 @@ module ActiveRecord
# member.update params[:member]
# member.avatar.icon # => 'sad'
#
+ # If you want to update the current avatar without providing the id, you must add <tt>:update_only</tt> option.
+ #
+ # class Member < ActiveRecord::Base
+ # has_one :avatar
+ # accepts_nested_attributes_for :avatar, update_only: true
+ # end
+ #
+ # params = { member: { avatar_attributes: { icon: 'sad' } } }
+ # member.update params[:member]
+ # member.avatar.id # => 2
+ # member.avatar.icon # => 'sad'
+ #
# By default you will only be able to set and update attributes on the
# associated model. If you want to destroy the associated model through the
# attributes hash, you have to enable it first using the
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index b48a137a73..c2393c1fc8 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -99,7 +99,9 @@ module ActiveRecord
# for updating all records in a single query.
def update(id = :all, attributes)
if id.is_a?(Array)
- id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }.compact
+ id.map { |one_id| find(one_id) }.each_with_index { |object, idx|
+ object.update(attributes[idx])
+ }
elsif id == :all
all.each { |record| record.update(attributes) }
else
@@ -112,7 +114,6 @@ module ActiveRecord
object.update(attributes)
object
end
- rescue RecordNotFound
end
# Destroy an object (or multiple objects) that has the given id. The object is instantiated first,
@@ -136,11 +137,10 @@ module ActiveRecord
# Todo.destroy(todos)
def destroy(id)
if id.is_a?(Array)
- id.map { |one_id| destroy(one_id) }.compact
+ find(id).each(&:destroy)
else
find(id).destroy
end
- rescue RecordNotFound
end
# Deletes the row with a primary key matching the +id+ argument, using a
@@ -165,6 +165,48 @@ module ActiveRecord
where(primary_key => id_or_array).delete_all
end
+ def _insert_record(values) # :nodoc:
+ primary_key_value = nil
+
+ if primary_key && Hash === values
+ primary_key_value = values[primary_key]
+
+ if !primary_key_value && prefetch_primary_key?
+ primary_key_value = next_sequence_value
+ values[primary_key] = primary_key_value
+ end
+ end
+
+ if values.empty?
+ im = arel_table.compile_insert(connection.empty_insert_statement_value(primary_key))
+ im.into arel_table
+ else
+ im = arel_table.compile_insert(_substitute_values(values))
+ end
+
+ connection.insert(im, "#{self} Create", primary_key || false, primary_key_value)
+ end
+
+ def _update_record(values, constraints) # :nodoc:
+ constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) }
+
+ um = arel_table.where(
+ constraints.reduce(&:and)
+ ).compile_update(_substitute_values(values), primary_key)
+
+ connection.update(um, "#{self} Update")
+ end
+
+ def _delete_record(constraints) # :nodoc:
+ constraints = _substitute_values(constraints).map { |attr, bind| attr.eq(bind) }
+
+ dm = Arel::DeleteManager.new
+ dm.from(arel_table)
+ dm.wheres = constraints
+
+ connection.delete(dm, "#{self} Destroy")
+ end
+
private
# Called by +instantiate+ to decide which class to use for a new
# record instance.
@@ -174,6 +216,14 @@ module ActiveRecord
def discriminate_class_for_record(record)
self
end
+
+ def _substitute_values(values)
+ values.map do |name, value|
+ attr = arel_attribute(name)
+ bind = predicate_builder.build_bind_attribute(name, value)
+ [attr, bind]
+ end
+ end
end
# Returns true if this object hasn't been saved yet -- that is, a record
@@ -271,7 +321,7 @@ module ActiveRecord
# callbacks or any <tt>:dependent</tt> association
# options, use <tt>#destroy</tt>.
def delete
- _relation_for_itself.delete_all if persisted?
+ _delete_row if persisted?
@destroyed = true
freeze
end
@@ -320,9 +370,10 @@ module ActiveRecord
# Any change to the attributes on either instance will affect both instances.
# If you want to change the sti column as well, use #becomes! instead.
def becomes(klass)
- became = klass.new
+ became = klass.allocate
+ became.send(:initialize)
became.instance_variable_set("@attributes", @attributes)
- became.instance_variable_set("@mutation_tracker", @mutation_tracker) if defined?(@mutation_tracker)
+ became.instance_variable_set("@mutations_from_database", @mutations_from_database) if defined?(@mutations_from_database)
became.instance_variable_set("@changed_attributes", attributes_changed_by_setter)
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
@@ -363,11 +414,7 @@ module ActiveRecord
verify_readonly_attribute(name)
public_send("#{name}=", value)
- if has_changes_to_save?
- save(validate: false)
- else
- true
- end
+ save(validate: false)
end
# Updates the attributes of the model from the passed-in hash and saves the
@@ -383,6 +430,7 @@ module ActiveRecord
end
alias update_attributes update
+ deprecate :update_attributes
# Updates its receiver just like #update but calls #save! instead
# of +save+, so an exception is raised if the record is invalid and saving will fail.
@@ -396,6 +444,7 @@ module ActiveRecord
end
alias update_attributes! update!
+ deprecate :update_attributes!
# Equivalent to <code>update_columns(name => value)</code>.
def update_column(name, value)
@@ -426,13 +475,16 @@ module ActiveRecord
verify_readonly_attribute(key.to_s)
end
- updated_count = _relation_for_itself.update_all(attributes)
+ affected_rows = self.class._update_record(
+ attributes,
+ self.class.primary_key => id_in_database
+ )
attributes.each do |k, v|
write_attribute_without_type_cast(k, v)
end
- updated_count == 1
+ affected_rows == 1
end
# Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1).
@@ -605,35 +657,12 @@ module ActiveRecord
MSG
end
- time ||= current_time_from_proper_timezone
- attributes = timestamp_attributes_for_update_in_model
- attributes.concat(names)
-
- unless attributes.empty?
- changes = {}
-
- attributes.each do |column|
- column = column.to_s
- changes[column] = write_attribute(column, time)
- end
-
- scope = _relation_for_itself
-
- if locking_enabled?
- locking_column = self.class.locking_column
- scope = scope.where(locking_column => read_attribute_before_type_cast(locking_column))
- changes[locking_column] = increment_lock
- end
-
- clear_attribute_changes(changes.keys)
- result = scope.update_all(changes) == 1
-
- if !result && locking_enabled?
- raise ActiveRecord::StaleObjectError.new(self, "touch")
- end
+ attribute_names = timestamp_attributes_for_update_in_model
+ attribute_names |= names.map(&:to_s)
- @_trigger_update_callback = result
- result
+ unless attribute_names.empty?
+ affected_rows = _touch_row(attribute_names, time)
+ @_trigger_update_callback = affected_rows == 1
else
true
end
@@ -646,19 +675,34 @@ module ActiveRecord
end
def destroy_row
- relation_for_destroy.delete_all
+ _delete_row
+ end
+
+ def _delete_row
+ self.class._delete_record(self.class.primary_key => id_in_database)
end
- def relation_for_destroy
- _relation_for_itself
+ def _touch_row(attribute_names, time)
+ time ||= current_time_from_proper_timezone
+
+ attribute_names.each do |attr_name|
+ write_attribute(attr_name, time)
+ clear_attribute_change(attr_name)
+ end
+
+ _update_row(attribute_names, "touch")
end
- def _relation_for_itself
- self.class.unscoped.where(self.class.primary_key => id)
+ def _update_row(attribute_names, attempted_action = "update")
+ self.class._update_record(
+ attributes_with_values(attribute_names),
+ self.class.primary_key => id_in_database
+ )
end
def create_or_update(*args, &block)
_raise_readonly_record_error if readonly?
+ return false if destroyed?
result = new_record? ? _create_record(&block) : _update_record(*args, &block)
result != false
end
@@ -666,26 +710,29 @@ module ActiveRecord
# Updates the associated record with values matching those of the instance attributes.
# Returns the number of affected rows.
def _update_record(attribute_names = self.attribute_names)
- attributes_values = arel_attributes_with_values_for_update(attribute_names)
- if attributes_values.empty?
- rows_affected = 0
+ attribute_names &= self.class.column_names
+ attribute_names = attributes_for_update(attribute_names)
+
+ if attribute_names.empty?
+ affected_rows = 0
@_trigger_update_callback = true
else
- rows_affected = self.class.unscoped._update_record attributes_values, id, id_in_database
- @_trigger_update_callback = rows_affected > 0
+ affected_rows = _update_row(attribute_names)
+ @_trigger_update_callback = affected_rows == 1
end
yield(self) if block_given?
- rows_affected
+ affected_rows
end
# Creates a record with values matching those of the instance attributes
# and returns its id.
def _create_record(attribute_names = self.attribute_names)
- attributes_values = arel_attributes_with_values_for_create(attribute_names)
+ attribute_names &= self.class.column_names
+ attributes_values = attributes_with_values_for_create(attribute_names)
- new_id = self.class.unscoped.insert attributes_values
+ new_id = self.class._insert_record(attributes_values)
self.id ||= new_id if self.class.primary_key
@new_record = false
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index 3d5babb8b7..28194c7c46 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -7,35 +7,31 @@ module ActiveRecord
# Enable the query cache within the block if Active Record is configured.
# If it's not, it will execute the given block.
def cache(&block)
- if configurations.empty?
- yield
- else
+ if connected? || !configurations.empty?
connection.cache(&block)
+ else
+ yield
end
end
# Disable the query cache within the block if Active Record is configured.
# If it's not, it will execute the given block.
def uncached(&block)
- if configurations.empty?
- yield
- else
+ if connected? || !configurations.empty?
connection.uncached(&block)
+ else
+ yield
end
end
end
def self.run
- caching_pool = ActiveRecord::Base.connection_pool
- caching_was_enabled = caching_pool.query_cache_enabled
-
- caching_pool.enable_query_cache!
-
- [caching_pool, caching_was_enabled]
+ ActiveRecord::Base.connection_handler.connection_pool_list.
+ reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! }
end
- def self.complete((caching_pool, caching_was_enabled))
- caching_pool.disable_query_cache! unless caching_was_enabled
+ def self.complete(pools)
+ pools.each { |pool| pool.disable_query_cache! }
ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
pool.release_connection if pool.active_connection? && !pool.connection.transaction_open?
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 3996d5661f..d33d36ac02 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -5,7 +5,7 @@ module ActiveRecord
delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, to: :all
delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all
delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all
- delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
+ delegate :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by, to: :all
delegate :find_by, :find_by!, to: :all
delegate :destroy_all, :delete_all, :update_all, to: :all
delegate :find_each, :find_in_batches, :in_batches, to: :all
@@ -13,7 +13,7 @@ module ActiveRecord
:where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending,
:having, :create_with, :distinct, :references, :none, :unscope, :merge, to: :all
delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
- delegate :pluck, :ids, to: :all
+ delegate :pluck, :pick, :ids, to: :all
# Executes a custom SQL query against your database and returns all the results. The results will
# be returned as an array with columns requested encapsulated as attributes of the model you call
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index ead42d64ec..6ab80a654d 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -53,16 +53,17 @@ module ActiveRecord
# to avoid cross references when loading a constant for the
# first time. Also, make it output to STDERR.
console do |app|
- require_relative "railties/console_sandbox" if app.sandbox?
- require_relative "base"
+ require "active_record/railties/console_sandbox" if app.sandbox?
+ require "active_record/base"
unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT)
console = ActiveSupport::Logger.new(STDERR)
Rails.logger.extend ActiveSupport::Logger.broadcast console
end
+ ActiveRecord::Base.verbose_query_logs = false
end
runner do
- require_relative "base"
+ require "active_record/base"
end
initializer "active_record.initialize_timezone" do
@@ -90,12 +91,16 @@ module ActiveRecord
filename = File.join(app.config.paths["db"].first, "schema_cache.yml")
if File.file?(filename)
+ current_version = ActiveRecord::Migrator.current_version
+
+ next if current_version.nil?
+
cache = YAML.load(File.read(filename))
- if cache.version == ActiveRecord::Migrator.current_version
+ if cache.version == current_version
connection.schema_cache = cache
connection_pool.schema_cache = cache.dup
else
- warn "Ignoring db/schema_cache.yml because it has expired. The current schema version is #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}."
+ warn "Ignoring db/schema_cache.yml because it has expired. The current schema version is #{current_version}, but the one in the cache is #{cache.version}."
end
end
end
@@ -106,7 +111,7 @@ module ActiveRecord
initializer "active_record.warn_on_records_fetched_greater_than" do
if config.active_record.warn_on_records_fetched_greater_than
ActiveSupport.on_load(:active_record) do
- require_relative "relation/record_fetch_warning"
+ require "active_record/relation/record_fetch_warning"
end
end
end
@@ -146,12 +151,19 @@ end_warning
# Expose database runtime to controller for logging.
initializer "active_record.log_runtime" do
- require_relative "railties/controller_runtime"
+ require "active_record/railties/controller_runtime"
ActiveSupport.on_load(:action_controller) do
include ActiveRecord::Railties::ControllerRuntime
end
end
+ initializer "active_record.collection_cache_association_loading" do
+ require "active_record/railties/collection_cache_association_loading"
+ ActiveSupport.on_load(:action_view) do
+ ActionView::PartialRenderer.prepend(ActiveRecord::Railties::CollectionCacheAssociationLoading)
+ end
+ end
+
initializer "active_record.set_reloader_hooks" do
ActiveSupport.on_load(:active_record) do
ActiveSupport::Reloader.before_class_unload do
@@ -177,7 +189,16 @@ end_warning
initializer "active_record.clear_active_connections" do
config.after_initialize do
ActiveSupport.on_load(:active_record) do
+ # Ideally the application doesn't connect to the database during boot,
+ # but sometimes it does. In case it did, we want to empty out the
+ # connection pools so that a non-database-using process (e.g. a master
+ # process in a forking server model) doesn't retain a needless
+ # connection. If it was needed, the incremental cost of reestablishing
+ # this connection is trivial: the rest of the pool would need to be
+ # populated anyway.
+
clear_active_connections!
+ flush_idle_connections!
end
end
end
diff --git a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb
new file mode 100644
index 0000000000..b5129e4239
--- /dev/null
+++ b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module ActiveRecord
+ module Railties # :nodoc:
+ module CollectionCacheAssociationLoading #:nodoc:
+ def setup(context, options, block)
+ @relation = relation_from_options(options)
+
+ super
+ end
+
+ def relation_from_options(cached: nil, partial: nil, collection: nil, **_)
+ return unless cached
+
+ relation = partial if partial.is_a?(ActiveRecord::Relation)
+ relation ||= collection if collection.is_a?(ActiveRecord::Relation)
+
+ if relation && !relation.loaded?
+ relation.skip_preloading!
+ end
+ end
+
+ def collection_without_template
+ @relation.preload_associations(@collection) if @relation
+ super
+ end
+
+ def collection_with_template
+ @relation.preload_associations(@collection) if @relation
+ super
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb
index 3cf66980a5..309441a057 100644
--- a/activerecord/lib/active_record/railties/controller_runtime.rb
+++ b/activerecord/lib/active_record/railties/controller_runtime.rb
@@ -1,56 +1,51 @@
# frozen_string_literal: true
require "active_support/core_ext/module/attr_internal"
-require_relative "../log_subscriber"
+require "active_record/log_subscriber"
module ActiveRecord
module Railties # :nodoc:
module ControllerRuntime #:nodoc:
extend ActiveSupport::Concern
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_internal :db_runtime
-
- private
-
- def process_action(action, *args)
- # We also need to reset the runtime before each action
- # because of queries in middleware or in cases we are streaming
- # and it won't be cleaned up by the method below.
- ActiveRecord::LogSubscriber.reset_runtime
- super
+ module ClassMethods # :nodoc:
+ def log_process_action(payload)
+ messages, db_runtime = super, payload[:db_runtime]
+ messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime
+ messages
+ end
end
- def cleanup_view_runtime
- if logger && logger.info? && ActiveRecord::Base.connected?
- db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime
- self.db_runtime = (db_runtime || 0) + db_rt_before_render
- runtime = super
- db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime
- self.db_runtime += db_rt_after_render
- runtime - db_rt_after_render
- else
+ private
+ attr_internal :db_runtime
+
+ def process_action(action, *args)
+ # We also need to reset the runtime before each action
+ # because of queries in middleware or in cases we are streaming
+ # and it won't be cleaned up by the method below.
+ ActiveRecord::LogSubscriber.reset_runtime
super
end
- end
- def append_info_to_payload(payload)
- super
- if ActiveRecord::Base.connected?
- payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime
+ def cleanup_view_runtime
+ if logger && logger.info? && ActiveRecord::Base.connected?
+ db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime
+ self.db_runtime = (db_runtime || 0) + db_rt_before_render
+ runtime = super
+ db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime
+ self.db_runtime += db_rt_after_render
+ runtime - db_rt_after_render
+ else
+ super
+ end
end
- end
- module ClassMethods # :nodoc:
- def log_process_action(payload)
- messages, db_runtime = super, payload[:db_runtime]
- messages << ("ActiveRecord: %.1fms" % db_runtime.to_f) if db_runtime
- messages
+ def append_info_to_payload(payload)
+ super
+ if ActiveRecord::Base.connected?
+ payload[:db_runtime] = (db_runtime || 0) + ActiveRecord::LogSubscriber.reset_runtime
+ end
end
- end
end
end
end
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 691b3612d8..24449e8df3 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -4,16 +4,16 @@ require "active_record"
db_namespace = namespace :db do
desc "Set the environment value for the database"
- task "environment:set" => [:environment, :load_config] do
+ task "environment:set" => :load_config do
ActiveRecord::InternalMetadata.create_table
- ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
+ ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Base.connection.migration_context.current_environment
end
- task check_protected_environments: [:environment, :load_config] do
+ task check_protected_environments: :load_config do
ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
end
- task :load_config do
+ task load_config: :environment do
ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {}
ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
end
@@ -22,6 +22,14 @@ db_namespace = namespace :db do
task all: :load_config do
ActiveRecord::Tasks::DatabaseTasks.create_all
end
+
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ desc "Create #{spec_name} database for current environment"
+ task spec_name => :load_config do
+ db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name)
+ ActiveRecord::Tasks::DatabaseTasks.create(db_config.config)
+ end
+ end
end
desc "Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to creating the development and test databases."
@@ -33,6 +41,14 @@ db_namespace = namespace :db do
task all: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.drop_all
end
+
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ desc "Drop #{spec_name} database for current environment"
+ task spec_name => [:load_config, :check_protected_environments] do
+ db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name)
+ ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config)
+ end
+ end
end
desc "Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to dropping the development and test databases."
@@ -56,8 +72,11 @@ db_namespace = namespace :db do
end
desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)."
- task migrate: [:environment, :load_config] do
- ActiveRecord::Tasks::DatabaseTasks.migrate
+ task migrate: :load_config do
+ ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config|
+ ActiveRecord::Base.establish_connection(config)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
db_namespace["_dump"].invoke
end
@@ -77,8 +96,17 @@ db_namespace = namespace :db do
end
namespace :migrate do
+ ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name|
+ desc "Migrate #{spec_name} database for current environment"
+ task spec_name => :load_config do
+ db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name)
+ ActiveRecord::Base.establish_connection(db_config.config)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
+ end
+
# desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).'
- task redo: [:environment, :load_config] do
+ task redo: :load_config do
raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty?
if ENV["VERSION"]
@@ -94,24 +122,33 @@ db_namespace = namespace :db do
task reset: ["db:drop", "db:create", "db:migrate"]
# desc 'Runs the "up" for a given migration VERSION.'
- task up: [:environment, :load_config] do
+ task up: :load_config do
raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty?
- version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
- ActiveRecord::Migrator.run(:up, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version)
+ ActiveRecord::Tasks::DatabaseTasks.check_target_version
+
+ ActiveRecord::Base.connection.migration_context.run(
+ :up,
+ ActiveRecord::Tasks::DatabaseTasks.target_version
+ )
db_namespace["_dump"].invoke
end
# desc 'Runs the "down" for a given migration VERSION.'
- task down: [:environment, :load_config] do
+ task down: :load_config do
raise "VERSION is required - To go down one migration, use db:rollback" if !ENV["VERSION"] || ENV["VERSION"].empty?
- version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
- ActiveRecord::Migrator.run(:down, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version)
+
+ ActiveRecord::Tasks::DatabaseTasks.check_target_version
+
+ ActiveRecord::Base.connection.migration_context.run(
+ :down,
+ ActiveRecord::Tasks::DatabaseTasks.target_version
+ )
db_namespace["_dump"].invoke
end
desc "Display status of migrations"
- task status: [:environment, :load_config] do
+ task status: :load_config do
unless ActiveRecord::SchemaMigration.table_exists?
abort "Schema migrations table does not exist yet."
end
@@ -120,8 +157,7 @@ db_namespace = namespace :db do
puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n"
puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name"
puts "-" * 50
- paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
- ActiveRecord::Migrator.migrations_status(paths).each do |status, version, name|
+ ActiveRecord::Base.connection.migration_context.migrations_status.each do |status, version, name|
puts "#{status.center(8)} #{version.ljust(14)} #{name}"
end
puts
@@ -129,16 +165,16 @@ db_namespace = namespace :db do
end
desc "Rolls the schema back to the previous version (specify steps w/ STEP=n)."
- task rollback: [:environment, :load_config] do
+ task rollback: :load_config do
step = ENV["STEP"] ? ENV["STEP"].to_i : 1
- ActiveRecord::Migrator.rollback(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step)
+ ActiveRecord::Base.connection.migration_context.rollback(step)
db_namespace["_dump"].invoke
end
# desc 'Pushes the schema to the next version (specify steps w/ STEP=n).'
- task forward: [:environment, :load_config] do
+ task forward: :load_config do
step = ENV["STEP"] ? ENV["STEP"].to_i : 1
- ActiveRecord::Migrator.forward(ActiveRecord::Tasks::DatabaseTasks.migrations_paths, step)
+ ActiveRecord::Base.connection.migration_context.forward(step)
db_namespace["_dump"].invoke
end
@@ -146,12 +182,12 @@ db_namespace = namespace :db do
task reset: [ "db:drop", "db:setup" ]
# desc "Retrieves the charset for the current environment's database"
- task charset: [:environment, :load_config] do
+ task charset: :load_config do
puts ActiveRecord::Tasks::DatabaseTasks.charset_current
end
# desc "Retrieves the collation for the current environment's database"
- task collation: [:environment, :load_config] do
+ task collation: :load_config do
begin
puts ActiveRecord::Tasks::DatabaseTasks.collation_current
rescue NoMethodError
@@ -160,13 +196,13 @@ db_namespace = namespace :db do
end
desc "Retrieves the current schema version number"
- task version: [:environment, :load_config] do
- puts "Current version: #{ActiveRecord::Migrator.current_version}"
+ task version: :load_config do
+ puts "Current version: #{ActiveRecord::Base.connection.migration_context.current_version}"
end
# desc "Raises an error if there are pending migrations"
- task abort_if_pending_migrations: [:environment, :load_config] do
- pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Tasks::DatabaseTasks.migrations_paths).pending_migrations
+ task abort_if_pending_migrations: :load_config do
+ pending_migrations = ActiveRecord::Base.connection.migration_context.open.pending_migrations
if pending_migrations.any?
puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}"
@@ -188,8 +224,8 @@ db_namespace = namespace :db do
namespace :fixtures do
desc "Loads fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
- task load: [:environment, :load_config] do
- require_relative "../fixtures"
+ task load: :load_config do
+ require "active_record/fixtures"
base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
@@ -210,8 +246,8 @@ db_namespace = namespace :db do
end
# desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures."
- task identify: [:environment, :load_config] do
- require_relative "../fixtures"
+ task identify: :load_config do
+ require "active_record/fixtures"
label, id = ENV["LABEL"], ENV["ID"]
raise "LABEL or ID required" if label.blank? && id.blank?
@@ -221,7 +257,7 @@ db_namespace = namespace :db do
base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path
Dir["#{base_dir}/**/*.yml"].each do |file|
- if data = YAML::load(ERB.new(IO.read(file)).result)
+ if data = YAML.load(ERB.new(IO.read(file)).result)
data.each_key do |key|
key_id = ActiveRecord::FixtureSet.identify(key)
@@ -236,17 +272,22 @@ db_namespace = namespace :db do
namespace :schema do
desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record"
- task dump: [:environment, :load_config] do
- require_relative "../schema_dumper"
- filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema.rb")
- File.open(filename, "w:utf-8") do |file|
- ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
+ task dump: :load_config do
+ require "active_record/schema_dumper"
+
+ ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config|
+ filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :ruby)
+ File.open(filename, "w:utf-8") do |file|
+ ActiveRecord::Base.establish_connection(config)
+ ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
+ end
end
+
db_namespace["schema:dump"].reenable
end
desc "Loads a schema.rb file into the database"
- task load: [:environment, :load_config, :check_protected_environments] do
+ task load: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV["SCHEMA"])
end
@@ -256,39 +297,41 @@ db_namespace = namespace :db do
namespace :cache do
desc "Creates a db/schema_cache.yml file."
- task dump: [:environment, :load_config] do
+ task dump: :load_config do
conn = ActiveRecord::Base.connection
filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml")
ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(conn, filename)
end
desc "Clears a db/schema_cache.yml file."
- task clear: [:environment, :load_config] do
+ task clear: :load_config do
filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml")
rm_f filename, verbose: false
end
end
-
end
namespace :structure do
desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql"
- task dump: [:environment, :load_config] do
- filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql")
- current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
-
- if ActiveRecord::SchemaMigration.table_exists?
- File.open(filename, "a") do |f|
- f.puts ActiveRecord::Base.connection.dump_schema_information
- f.print "\n"
+ task dump: :load_config do
+ ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config|
+ ActiveRecord::Base.establish_connection(config)
+ filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :sql)
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, filename)
+
+ if ActiveRecord::SchemaMigration.table_exists?
+ File.open(filename, "a") do |f|
+ f.puts ActiveRecord::Base.connection.dump_schema_information
+ f.print "\n"
+ end
end
end
+
db_namespace["structure:dump"].reenable
end
desc "Recreates the databases from the structure.sql file"
- task load: [:environment, :load_config, :check_protected_environments] do
+ task load: [:load_config, :check_protected_environments] do
ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV["SCHEMA"])
end
@@ -313,7 +356,7 @@ db_namespace = namespace :db do
begin
should_reconnect = ActiveRecord::Base.connection_pool.active_connection?
ActiveRecord::Schema.verbose = false
- ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :ruby, ENV["SCHEMA"]
+ ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :ruby, ENV["SCHEMA"], "test"
ensure
if should_reconnect
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env])
@@ -323,16 +366,16 @@ db_namespace = namespace :db do
# desc "Recreate the test database from an existent structure.sql file"
task load_structure: %w(db:test:purge) do
- ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"]
+ ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"], "test"
end
# desc "Empty the test database"
- task purge: %w(environment load_config check_protected_environments) do
+ task purge: %w(load_config check_protected_environments) do
ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations["test"]
end
# desc 'Load the test schema'
- task prepare: %w(environment load_config) do
+ task prepare: :load_config do
unless ActiveRecord::Base.configurations.blank?
db_namespace["test:load"].invoke
end
diff --git a/activerecord/lib/active_record/railties/jdbcmysql_error.rb b/activerecord/lib/active_record/railties/jdbcmysql_error.rb
deleted file mode 100644
index 72c75ddd52..0000000000
--- a/activerecord/lib/active_record/railties/jdbcmysql_error.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-#FIXME Remove if ArJdbcMysql will give.
-module ArJdbcMySQL #:nodoc:
- class Error < StandardError #:nodoc:
- attr_accessor :error_number, :sql_state
-
- def initialize(msg)
- super
- @error_number = nil
- @sql_state = nil
- end
-
- # Mysql gem compatibility
- alias_method :errno, :error_number
- alias_method :error, :message
- end
-end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 82ab2415e1..2f43d005f3 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,7 +1,6 @@
# frozen_string_literal: true
require "active_support/core_ext/string/filters"
-require "active_support/deprecation"
require "concurrent/map"
module ActiveRecord
@@ -35,7 +34,8 @@ module ActiveRecord
def self.add_reflection(ar, name, reflection)
ar.clear_reflections_cache
- ar._reflections = ar._reflections.merge(name.to_s => reflection)
+ name = name.to_s
+ ar._reflections = ar._reflections.except(name).merge!(name => reflection)
end
def self.add_aggregate_reflection(ar, name, reflection)
@@ -174,11 +174,6 @@ module ActiveRecord
scope ? [scope] : []
end
- def scope_chain
- chain.map(&:scopes)
- end
- deprecate :scope_chain
-
def build_join_constraint(table, foreign_table)
key = join_keys.key
foreign_key = join_keys.foreign_key
@@ -198,7 +193,7 @@ module ActiveRecord
klass_scope = klass_join_scope(table, predicate_builder)
if type
- klass_scope.where!(type => foreign_klass.base_class.sti_name)
+ klass_scope.where!(type => foreign_klass.polymorphic_name)
end
scope_chain_items.inject(klass_scope, &:merge!)
@@ -296,10 +291,14 @@ module ActiveRecord
end
def build_scope(table, predicate_builder = predicate_builder(table))
- Relation.create(klass, table, predicate_builder)
+ Relation.create(
+ klass,
+ table: table,
+ predicate_builder: predicate_builder
+ )
end
- def join_primary_key(_)
+ def join_primary_key(*)
foreign_key
end
@@ -417,6 +416,9 @@ module ActiveRecord
# Active Record class.
class AssociationReflection < MacroReflection #:nodoc:
def compute_class(name)
+ if polymorphic?
+ raise ArgumentError, "Polymorphic association does not support to compute class."
+ end
active_record.send(:compute_type, name)
end
@@ -425,21 +427,13 @@ module ActiveRecord
def initialize(name, scope, options, active_record)
super
- @automatic_inverse_of = nil
@type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type")
- @foreign_type = options[:foreign_type] || "#{name}_type"
+ @foreign_type = options[:polymorphic] && (options[:foreign_type] || "#{name}_type")
@constructable = calculate_constructable(macro, options)
@association_scope_cache = Concurrent::Map.new
if options[:class_name] && options[:class_name].class == Class
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
- Passing a class to the `class_name` is deprecated and will raise
- an ArgumentError in Rails 5.2. It eagerloads more classes than
- necessary and potentially creates circular dependencies.
-
- Please pass the class name as a string:
- `#{macro} :#{name}, class_name: '#{options[:class_name]}'`
- MSG
+ raise ArgumentError, "A class was passed to `:class_name` but we are expecting a string."
end
end
@@ -472,10 +466,6 @@ module ActiveRecord
options[:primary_key] || primary_key(klass || self.klass)
end
- def association_primary_key_type
- klass.type_for_attribute(association_primary_key.to_s)
- end
-
def active_record_primary_key
@active_record_primary_key ||= options[:primary_key] || primary_key(active_record)
end
@@ -581,7 +571,7 @@ module ActiveRecord
end
VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to]
- INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :foreign_key]
+ INVALID_AUTOMATIC_INVERSE_OPTIONS = [:through, :foreign_key]
def add_as_source(seed)
seed
@@ -609,12 +599,14 @@ module ActiveRecord
# If it cannot find a suitable inverse association name, it returns
# +nil+.
def inverse_name
- options.fetch(:inverse_of) do
- @automatic_inverse_of ||= automatic_inverse_of
+ unless defined?(@inverse_name)
+ @inverse_name = options.fetch(:inverse_of) { automatic_inverse_of }
end
+
+ @inverse_name
end
- # returns either false or the inverse association name that it finds.
+ # returns either +nil+ or the inverse association name that it finds.
def automatic_inverse_of
if can_find_inverse_of_automatically?(self)
inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym
@@ -631,17 +623,12 @@ module ActiveRecord
return inverse_name
end
end
-
- false
end
# Checks if the inverse reflection that is returned from the
# +automatic_inverse_of+ method is a valid reflection. We must
# make sure that the reflection's active_record name matches up
# with the current reflection's klass name.
- #
- # Note: klass will always be valid because when there's a NameError
- # from calling +klass+, +reflection+ will already be set to false.
def valid_inverse_reflection?(reflection)
reflection &&
klass <= reflection.active_record &&
@@ -736,7 +723,7 @@ module ActiveRecord
end
end
- def join_primary_key(klass)
+ def join_primary_key(klass = nil)
polymorphic? ? association_primary_key(klass) : association_primary_key
end
@@ -745,6 +732,9 @@ module ActiveRecord
end
private
+ def can_find_inverse_of_automatically?(_)
+ !polymorphic? && super
+ end
def calculate_constructable(macro, options)
!polymorphic?
@@ -853,10 +843,6 @@ module ActiveRecord
source_reflection.join_scopes(table, predicate_builder) + super
end
- def source_type_scope
- through_reflection.klass.where(foreign_type => options[:source_type])
- end
-
def has_scope?
scope || options[:source_type] ||
source_reflection.has_scope? ||
@@ -877,10 +863,6 @@ module ActiveRecord
actual_source_reflection.options[:primary_key] || primary_key(klass || self.klass)
end
- def association_primary_key_type
- klass.type_for_attribute(association_primary_key.to_s)
- end
-
# Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form.
#
# class Post < ActiveRecord::Base
@@ -983,16 +965,14 @@ module ActiveRecord
collect_join_reflections(seed + [self])
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
protected
- attr_reader :delegate_reflection
-
def actual_source_reflection # FIXME: this is a horrible name
source_reflection.actual_source_reflection
end
private
+ attr_reader :delegate_reflection
+
def collect_join_reflections(seed)
a = source_reflection.add_as_source seed
if options[:source_type]
@@ -1016,29 +996,24 @@ module ActiveRecord
end
class PolymorphicReflection < AbstractReflection # :nodoc:
- delegate :klass, :scope, :plural_name, :type, :get_join_keys, to: :@reflection
+ delegate :klass, :scope, :plural_name, :type, :get_join_keys, :scope_for, to: :@reflection
def initialize(reflection, previous_reflection)
@reflection = reflection
@previous_reflection = previous_reflection
end
- def scopes
- scopes = @previous_reflection.scopes
- scopes << @previous_reflection.source_type_scope
- end
-
def join_scopes(table, predicate_builder) # :nodoc:
scopes = @previous_reflection.join_scopes(table, predicate_builder) + super
- scopes << @previous_reflection.source_type_scope
+ scopes << build_scope(table, predicate_builder).instance_exec(nil, &source_type_scope)
end
def constraints
- @reflection.constraints + [source_type_info]
+ @reflection.constraints + [source_type_scope]
end
private
- def source_type_info
+ def source_type_scope
type = @previous_reflection.foreign_type
source_type = @previous_reflection.options[:source_type]
lambda { |object| where(type => source_type) }
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index f6825e67d7..c055b97061 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -9,8 +9,9 @@ module ActiveRecord
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering,
:reverse_order, :distinct, :create_with, :skip_query_cache]
+
CLAUSE_METHODS = [:where, :having, :from]
- INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having]
+ INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having]
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS
@@ -18,17 +19,19 @@ module ActiveRecord
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
attr_reader :table, :klass, :loaded, :predicate_builder
+ attr_accessor :skip_preloading_value
alias :model :klass
alias :loaded? :loaded
alias :locked? :lock_value
- def initialize(klass, table, predicate_builder, values = {})
+ def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
@klass = klass
@table = table
@values = values
@offsets = {}
@loaded = false
@predicate_builder = predicate_builder
+ @delegate_to_klass = false
end
def initialize_copy(other)
@@ -36,67 +39,6 @@ module ActiveRecord
reset
end
- def insert(values) # :nodoc:
- primary_key_value = nil
-
- if primary_key && Hash === values
- primary_key_value = values[values.keys.find { |k|
- k.name == primary_key
- }]
-
- if !primary_key_value && klass.prefetch_primary_key?
- primary_key_value = klass.next_sequence_value
- values[arel_attribute(klass.primary_key)] = primary_key_value
- end
- end
-
- im = arel.create_insert
- im.into @table
-
- substitutes = substitute_values values
-
- if values.empty? # empty insert
- im.values = Arel.sql(connection.empty_insert_statement_value)
- else
- im.insert substitutes
- end
-
- @klass.connection.insert(
- im,
- "SQL",
- primary_key || false,
- primary_key_value,
- nil,
- )
- end
-
- def _update_record(values, id, id_was) # :nodoc:
- substitutes = substitute_values values
-
- scope = @klass.unscoped
-
- if @klass.finder_needs_type_condition?
- scope.unscope!(where: @klass.inheritance_column)
- end
-
- relation = scope.where(@klass.primary_key => (id_was || id))
- um = relation
- .arel
- .compile_update(substitutes, @klass.primary_key)
-
- @klass.connection.update(
- um,
- "SQL",
- )
- end
-
- def substitute_values(values) # :nodoc:
- values.map do |arel_attr, value|
- bind = predicate_builder.build_bind_attribute(arel_attr.name, value)
- [arel_attr, bind]
- end
- end
-
def arel_attribute(name) # :nodoc:
klass.arel_attribute(name, table)
end
@@ -113,8 +55,8 @@ module ActiveRecord
#
# user = users.new { |user| user.name = 'Oscar' }
# user.name # => Oscar
- def new(*args, &block)
- scoping { @klass.new(*args, &block) }
+ def new(attributes = nil, &block)
+ scoping { klass.new(scope_for_create(attributes), &block) }
end
alias build new
@@ -138,8 +80,12 @@ module ActiveRecord
#
# users.create(name: nil) # validation on name
# # => #<User id: nil, name: nil, ...>
- def create(*args, &block)
- scoping { @klass.create(*args, &block) }
+ def create(attributes = nil, &block)
+ if attributes.is_a?(Array)
+ attributes.collect { |attr| create(attr, &block) }
+ else
+ scoping { klass.create(scope_for_create(attributes), &block) }
+ end
end
# Similar to #create, but calls
@@ -148,8 +94,12 @@ module ActiveRecord
#
# Expects arguments in the same format as
# {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!].
- def create!(*args, &block)
- scoping { @klass.create!(*args, &block) }
+ def create!(attributes = nil, &block)
+ if attributes.is_a?(Array)
+ attributes.collect { |attr| create!(attr, &block) }
+ else
+ scoping { klass.create!(scope_for_create(attributes), &block) }
+ end
end
def first_or_create(attributes = nil, &block) # :nodoc:
@@ -195,23 +145,12 @@ module ActiveRecord
# failed due to validation errors it won't be persisted, you get what
# #create returns in such situation.
#
- # Please note *this method is not atomic*, it runs first a SELECT, and if
+ # Please note <b>this method is not atomic</b>, it runs first a SELECT, and if
# there are no results an INSERT is attempted. If there are other threads
# or processes there is a race condition between both calls and it could
# be the case that you end up with two similar records.
#
- # Whether that is a problem or not depends on the logic of the
- # application, but in the particular case in which rows have a UNIQUE
- # constraint an exception may be raised, just retry:
- #
- # begin
- # CreditAccount.transaction(requires_new: true) do
- # CreditAccount.find_or_create_by(user_id: user.id)
- # end
- # rescue ActiveRecord::RecordNotUnique
- # retry
- # end
- #
+ # If this might be a problem for your application, please see #create_or_find_by.
def find_or_create_by(attributes, &block)
find_by(attributes) || create(attributes, &block)
end
@@ -223,6 +162,47 @@ module ActiveRecord
find_by(attributes) || create!(attributes, &block)
end
+ # Attempts to create a record with the given attributes in a table that has a unique constraint
+ # on one or several of its columns. If a row already exists with one or several of these
+ # unique constraints, the exception such an insertion would normally raise is caught,
+ # and the existing record with those attributes is found using #find_by.
+ #
+ # This is similar to #find_or_create_by, but avoids the problem of stale reads between the SELECT
+ # and the INSERT, as that method needs to first query the table, then attempt to insert a row
+ # if none is found.
+ #
+ # There are several drawbacks to #create_or_find_by, though:
+ #
+ # * The underlying table must have the relevant columns defined with unique constraints.
+ # * A unique constraint violation may be triggered by only one, or at least less than all,
+ # of the given attributes. This means that the subsequent #find_by may fail to find a
+ # matching record, which will then raise an <tt>ActiveRecord::RecordNotFound</tt> exception,
+ # rather than a record with the given attributes.
+ # * While we avoid the race condition between SELECT -> INSERT from #find_or_create_by,
+ # we actually have another race condition between INSERT -> SELECT, which can be triggered
+ # if a DELETE between those two statements is run by another client. But for most applications,
+ # that's a significantly less likely condition to hit.
+ # * It relies on exception handling to handle control flow, which may be marginally slower.
+ #
+ # This method will return a record if all given attributes are covered by unique constraints
+ # (unless the INSERT -> DELETE -> SELECT race condition is triggered), but if creation was attempted
+ # and failed due to validation errors it won't be persisted, you get what #create returns in
+ # such situation.
+ def create_or_find_by(attributes, &block)
+ transaction(requires_new: true) { create(attributes, &block) }
+ rescue ActiveRecord::RecordNotUnique
+ find_by!(attributes)
+ end
+
+ # Like #create_or_find_by, but calls
+ # {create!}[rdoc-ref:Persistence::ClassMethods#create!] so an exception
+ # is raised if the created record is invalid.
+ def create_or_find_by!(attributes, &block)
+ transaction(requires_new: true) { create!(attributes, &block) }
+ rescue ActiveRecord::RecordNotUnique
+ find_by!(attributes)
+ end
+
# Like #find_or_create_by, but calls {new}[rdoc-ref:Core#new]
# instead of {create}[rdoc-ref:Persistence::ClassMethods#create].
def find_or_initialize_by(attributes, &block)
@@ -243,9 +223,10 @@ module ActiveRecord
end
# Converts relation objects to Array.
- def to_a
+ def to_ary
records.dup
end
+ alias to_a to_ary
def records # :nodoc:
load
@@ -334,6 +315,13 @@ module ActiveRecord
klass.current_scope = previous
end
+ def _exec_scope(*args, &block) # :nodoc:
+ @delegate_to_klass = true
+ instance_exec(*args, &block) || self
+ ensure
+ @delegate_to_klass = false
+ end
+
# Updates all records in the current relation with details given. This method constructs a single SQL UPDATE
# statement and sends it straight to the database. It does not instantiate the involved models and it does not
# trigger Active Record callbacks or validations. However, values passed to #update_all will still go through
@@ -359,12 +347,17 @@ module ActiveRecord
def update_all(updates)
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
+ if eager_loading?
+ relation = apply_join_dependency
+ return relation.update_all(updates)
+ end
+
stmt = Arel::UpdateManager.new
- stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates))
+ stmt.set Arel.sql(@klass.sanitize_sql_for_assignment(updates))
stmt.table(table)
- if has_join_values?
+ if has_join_values? || offset_value
@klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key))
else
stmt.key = arel_attribute(primary_key)
@@ -373,7 +366,44 @@ module ActiveRecord
stmt.wheres = arel.constraints
end
- @klass.connection.update stmt, "SQL"
+ @klass.connection.update stmt, "#{@klass} Update All"
+ end
+
+ # Touches all records in the current relation without instantiating records first with the updated_at/on attributes
+ # set to the current time or the time specified.
+ # This method can be passed attribute names and an optional time argument.
+ # If attribute names are passed, they are updated along with updated_at/on attributes.
+ # If no time argument is passed, the current time is used as default.
+ #
+ # === Examples
+ #
+ # # Touch all records
+ # Person.all.touch_all
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670'"
+ #
+ # # Touch multiple records with a custom attribute
+ # Person.all.touch_all(:created_at)
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670', \"created_at\" = '2018-01-04 22:55:23.132670'"
+ #
+ # # Touch multiple records with a specified time
+ # Person.all.touch_all(time: Time.new(2020, 5, 16, 0, 0, 0))
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2020-05-16 00:00:00'"
+ #
+ # # Touch records with scope
+ # Person.where(name: 'David').touch_all
+ # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670' WHERE \"people\".\"name\" = 'David'"
+ def touch_all(*names, time: nil)
+ attributes = Array(names) + klass.timestamp_attributes_for_update_in_model
+ time ||= klass.current_time_from_proper_timezone
+ updates = {}
+ attributes.each { |column| updates[column] = time }
+
+ if klass.locking_enabled?
+ quoted_locking_column = connection.quote_column_name(klass.locking_column)
+ updates = sanitize_sql_for_assignment(updates) + ", #{quoted_locking_column} = COALESCE(#{quoted_locking_column}, 0) + 1"
+ end
+
+ update_all(updates)
end
# Destroys the records by instantiating each
@@ -412,8 +442,8 @@ module ActiveRecord
#
# If an invalid method is supplied, #delete_all raises an ActiveRecordError:
#
- # Post.limit(100).delete_all
- # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit
+ # Post.distinct.delete_all
+ # # => ActiveRecord::ActiveRecordError: delete_all doesn't support distinct
def delete_all
invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method|
value = get_value(method)
@@ -423,16 +453,21 @@ module ActiveRecord
raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}")
end
+ if eager_loading?
+ relation = apply_join_dependency
+ return relation.delete_all
+ end
+
stmt = Arel::DeleteManager.new
stmt.from(table)
- if has_join_values?
+ if has_join_values? || has_limit_or_offset?
@klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key))
else
stmt.wheres = arel.constraints
end
- affected = @klass.connection.delete(stmt, "SQL")
+ affected = @klass.connection.delete(stmt, "#{@klass} Destroy")
reset
affected
@@ -457,6 +492,7 @@ module ActiveRecord
end
def reset
+ @delegate_to_klass = false
@to_sql = @arel = @loaded = @should_eager_load = nil
@records = [].freeze
@offsets = {}
@@ -472,7 +508,7 @@ module ActiveRecord
relation = self
if eager_loading?
- find_with_associations { |rel, _| relation = rel }
+ apply_join_dependency { |rel, _| relation = rel }
end
conn = klass.connection
@@ -490,8 +526,10 @@ module ActiveRecord
where_clause.to_h(relation_table_name)
end
- def scope_for_create
- where_values_hash.merge!(create_with_value.stringify_keys)
+ def scope_for_create(attributes = nil)
+ scope = where_values_hash.merge!(create_with_value.stringify_keys)
+ scope.merge!(attributes) if attributes
+ scope
end
# Returns true if relation needs eager loading.
@@ -551,6 +589,21 @@ module ActiveRecord
limit_value || offset_value
end
+ def alias_tracker(joins = [], aliases = nil) # :nodoc:
+ joins += [aliases] if aliases
+ ActiveRecord::Associations::AliasTracker.create(connection, table.name, joins)
+ end
+
+ def preload_associations(records) # :nodoc:
+ preload = preload_values
+ preload += includes_values unless eager_loading?
+ preloader = nil
+ preload.each do |associations|
+ preloader ||= build_preloader
+ preloader.preload records, associations
+ end
+ end
+
protected
def load_records(records)
@@ -568,7 +621,7 @@ module ActiveRecord
skip_query_cache_if_necessary do
@records =
if eager_loading?
- find_with_associations do |relation, join_dependency|
+ apply_join_dependency do |relation, join_dependency|
if ActiveRecord::NullRelation === relation
[]
else
@@ -580,13 +633,7 @@ module ActiveRecord
klass.find_by_sql(arel, &block).freeze
end
- preload = preload_values
- preload += includes_values unless eager_loading?
- preloader = nil
- preload.each do |associations|
- preloader ||= build_preloader
- preloader.preload @records, associations
- end
+ preload_associations(@records) unless skip_preloading_value
@records.each(&:readonly!) if readonly_value
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index 356ad0dcd6..ec4bb06c57 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-require_relative "batches/batch_enumerator"
+require "active_record/relation/batches/batch_enumerator"
module ActiveRecord
module Batches
@@ -251,25 +251,31 @@ module ActiveRecord
end
end
- attr = Relation::QueryAttribute.new(primary_key, primary_key_offset, klass.type_for_attribute(primary_key))
- batch_relation = relation.where(arel_attribute(primary_key).gt(Arel::Nodes::BindParam.new(attr)))
+ bind = primary_key_bind(primary_key_offset)
+ batch_relation = relation.where(arel_attribute(primary_key).gt(bind))
end
end
private
def apply_limits(relation, start, finish)
- if start
- attr = Relation::QueryAttribute.new(primary_key, start, klass.type_for_attribute(primary_key))
- relation = relation.where(arel_attribute(primary_key).gteq(Arel::Nodes::BindParam.new(attr)))
- end
- if finish
- attr = Relation::QueryAttribute.new(primary_key, finish, klass.type_for_attribute(primary_key))
- relation = relation.where(arel_attribute(primary_key).lteq(Arel::Nodes::BindParam.new(attr)))
- end
+ relation = apply_start_limit(relation, start) if start
+ relation = apply_finish_limit(relation, finish) if finish
relation
end
+ def apply_start_limit(relation, start)
+ relation.where(arel_attribute(primary_key).gteq(primary_key_bind(start)))
+ end
+
+ def apply_finish_limit(relation, finish)
+ relation.where(arel_attribute(primary_key).lteq(primary_key_bind(finish)))
+ end
+
+ def primary_key_bind(value)
+ predicate_builder.build_bind_attribute(primary_key, value)
+ end
+
def batch_order
arel_attribute(primary_key).asc
end
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 42d43224fa..f215c95f51 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -130,8 +130,15 @@ module ActiveRecord
# end
def calculate(operation, column_name)
if has_include?(column_name)
- relation = construct_relation_for_association_calculations
- relation.distinct! if operation.to_s.downcase == "count"
+ relation = apply_join_dependency
+
+ if operation.to_s.downcase == "count"
+ relation.distinct!
+ # PostgreSQL: ORDER BY expressions must appear in SELECT list when using DISTINCT
+ if (column_name == :all || column_name.nil?) && select_values.empty?
+ relation.order_values = []
+ end
+ end
relation.calculate(operation, column_name)
else
@@ -180,8 +187,10 @@ module ActiveRecord
end
if has_include?(column_names.first)
- construct_relation_for_association_calculations.pluck(*column_names)
+ relation = apply_join_dependency
+ relation.pluck(*column_names)
else
+ enforce_raw_sql_whitelist(column_names)
relation = spawn
relation.select_values = column_names.map { |cn|
@klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn
@@ -191,6 +200,24 @@ module ActiveRecord
end
end
+ # Pick the value(s) from the named column(s) in the current relation.
+ # This is short-hand for <tt>relation.limit(1).pluck(*column_names).first</tt>, and is primarily useful
+ # when you have a relation that's already narrowed down to a single row.
+ #
+ # Just like #pluck, #pick will only load the actual value, not the entire record object, so it's also
+ # more efficient. The value is, again like with pluck, typecast by the column type.
+ #
+ # Person.where(id: 1).pick(:name)
+ # # SELECT people.name FROM people WHERE id = 1 LIMIT 1
+ # # => 'David'
+ #
+ # Person.where(id: 1).pick(:name, :email_address)
+ # # SELECT people.name, people.email_address FROM people WHERE id = 1 LIMIT 1
+ # # => [ 'David', 'david@loudthinking.com' ]
+ def pick(*column_names)
+ limit(1).pluck(*column_names).first
+ end
+
# Pluck all the ID's for the relation using the table's primary key
#
# Person.ids # SELECT people.id FROM people
@@ -215,7 +242,7 @@ module ActiveRecord
if operation == "count"
column_name ||= select_for_count
if column_name == :all
- if distinct && !(has_limit_or_offset? && order_values.any?)
+ if distinct && (group_values.any? || select_values.empty? && order_values.empty?)
column_name = primary_key
end
elsif column_name =~ /\s*DISTINCT[\s(]+/i
@@ -233,7 +260,7 @@ module ActiveRecord
def aggregate_column(column_name)
return column_name if Arel::Expressions === column_name
- if @klass.has_attribute?(column_name.to_s) || @klass.attribute_alias?(column_name.to_s)
+ if @klass.has_attribute?(column_name) || @klass.attribute_alias?(column_name)
@klass.arel_attribute(column_name)
else
Arel.sql(column_name == :all ? "*" : column_name.to_s)
@@ -247,7 +274,7 @@ module ActiveRecord
def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
column_alias = column_name
- if operation == "count" && has_limit_or_offset?
+ if operation == "count" && (column_name == :all && distinct || has_limit_or_offset?)
# Shortcut when limit is zero.
return 0 if limit_value == 0
@@ -389,14 +416,12 @@ module ActiveRecord
end
def build_count_subquery(relation, column_name, distinct)
- relation.select_values = [
- if column_name == :all
- distinct ? table[Arel.star] : Arel.sql("1")
- else
- column_alias = Arel.sql("count_column")
- aggregate_column(column_name).as(column_alias)
- end
- ]
+ if column_name == :all
+ relation.select_values = [ Arel.sql(FinderMethods::ONE_AS_ONE) ] unless distinct
+ else
+ column_alias = Arel.sql("count_column")
+ relation.select_values = [ aggregate_column(column_name).as(column_alias) ]
+ end
subquery = relation.arel.as(Arel.sql("subquery_for_count"))
select_value = operation_over_aggregate_column(column_alias || Arel.star, "count", false)
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index 48af777b69..488f71cdde 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -38,7 +38,7 @@ module ActiveRecord
# may vary depending on the klass of a relation, so we create a subclass of Relation
# for each different klass, and the delegations are compiled into that subclass only.
- delegate :to_xml, :encode_with, :length, :each, :uniq, :to_ary, :join,
+ delegate :to_xml, :encode_with, :length, :each, :join,
:[], :&, :|, :+, :-, :sample, :reverse, :rotate, :compact, :in_groups, :in_groups_of,
:to_sentence, :to_formatted_s, :as_json,
:shuffle, :split, :slice, :index, :rindex, to: :records
@@ -82,6 +82,11 @@ module ActiveRecord
if @klass.respond_to?(method)
self.class.delegate_to_scoped_klass(method)
scoping { @klass.public_send(method, *args, &block) }
+ elsif @delegate_to_klass && @klass.respond_to?(method, true)
+ ActiveSupport::Deprecation.warn \
+ "Delegating missing #{method} method to #{@klass}. " \
+ "Accessibility of private/protected class methods in :scope is deprecated and will be removed in Rails 6.0."
+ @klass.send(method, *args, &block)
elsif arel.respond_to?(method)
ActiveSupport::Deprecation.warn \
"Delegating #{method} to arel is deprecated and will be removed in Rails 6.0."
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index c92d5a52f4..f7613a187d 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -18,9 +18,10 @@ module ActiveRecord
# Person.find([1]) # returns an array for the object with ID = 1
# Person.where("administrator = 1").order("created_on DESC").find(1)
#
- # NOTE: The returned records may not be in the same order as the ids you
- # provide since database rows are unordered. You will need to provide an explicit QueryMethods#order
- # option if you want the results to be sorted.
+ # NOTE: The returned records are in the same order as the ids you provide.
+ # If you want the results to be sorted by database, you can use ActiveRecord::QueryMethods#where
+ # method and provide an explicit ActiveRecord::QueryMethods#order option.
+ # But ActiveRecord::QueryMethods#where method doesn't raise ActiveRecord::RecordNotFound.
#
# ==== Find with lock
#
@@ -88,7 +89,7 @@ module ActiveRecord
where(arg, *args).take!
rescue ::RangeError
raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value",
- @klass.name)
+ @klass.name, @klass.primary_key)
end
# Gives a record (or N records if a parameter is supplied) without any implied
@@ -147,7 +148,7 @@ module ActiveRecord
#
# [#<Person id:4>, #<Person id:3>, #<Person id:2>]
def last(limit = nil)
- return find_last(limit) if loaded? || limit_value
+ return find_last(limit) if loaded? || has_limit_or_offset?
result = ordered_relation.limit(limit)
result = result.reverse_order!
@@ -284,7 +285,7 @@ module ActiveRecord
# * Hash - Finds the record that matches these +find+-style conditions
# (such as <tt>{name: 'David'}</tt>).
# * +false+ - Returns always +false+.
- # * No args - Returns +false+ if the table is empty, +true+ otherwise.
+ # * No args - Returns +false+ if the relation is empty, +true+ otherwise.
#
# For more information about specifying conditions as a hash or array,
# see the Conditions section in the introduction to ActiveRecord::Base.
@@ -300,6 +301,7 @@ module ActiveRecord
# Person.exists?(name: 'David')
# Person.exists?(false)
# Person.exists?
+ # Person.where(name: 'Spartacus', rating: 4).exists?
def exists?(conditions = :none)
if Base === conditions
raise ArgumentError, <<-MSG.squish
@@ -310,12 +312,12 @@ module ActiveRecord
return false if !conditions || limit_value == 0
- relation = self unless eager_loading?
- relation ||= apply_join_dependency(self, construct_join_dependency(eager_loading: false))
-
- return false if ActiveRecord::NullRelation === relation
+ if eager_loading?
+ relation = apply_join_dependency(eager_loading: false)
+ return relation.exists?(conditions)
+ end
- relation = construct_relation_for_exists(relation, conditions)
+ relation = construct_relation_for_exists(conditions)
skip_query_cache_if_necessary { connection.select_value(relation.arel, "#{name} Exists") } ? true : false
rescue ::RangeError
@@ -338,7 +340,7 @@ module ActiveRecord
if ids.nil?
error = "Couldn't find #{name}".dup
error << " with#{conditions}" if conditions
- raise RecordNotFound.new(error, name)
+ raise RecordNotFound.new(error, name, key)
elsif Array(ids).size == 1
error = "Couldn't find #{name} with '#{key}'=#{ids}#{conditions}"
raise RecordNotFound.new(error, name, key, ids)
@@ -346,7 +348,7 @@ module ActiveRecord
error = "Couldn't find all #{name.pluralize} with '#{key}': ".dup
error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})."
error << " Couldn't find #{name.pluralize(not_found_ids.size)} with #{key.to_s.pluralize(not_found_ids.size)} #{not_found_ids.join(', ')}." if not_found_ids
- raise RecordNotFound.new(error, name, primary_key, ids)
+ raise RecordNotFound.new(error, name, key, ids)
end
end
@@ -356,27 +358,8 @@ module ActiveRecord
offset_value || 0
end
- def find_with_associations
- # NOTE: the JoinDependency constructed here needs to know about
- # any joins already present in `self`, so pass them in
- #
- # failing to do so means that in cases like activerecord/test/cases/associations/inner_join_association_test.rb:136
- # incorrect SQL is generated. In that case, the join dependency for
- # SpecialCategorizations is constructed without knowledge of the
- # preexisting join in joins_values to categorizations (by way of
- # the `has_many :through` for categories).
- #
- join_dependency = construct_join_dependency(joins_values)
-
- aliases = join_dependency.aliases
- relation = select aliases.columns
- relation = apply_join_dependency(relation, join_dependency)
-
- yield relation, join_dependency
- end
-
- def construct_relation_for_exists(relation, conditions)
- relation = relation.except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1)
+ def construct_relation_for_exists(conditions)
+ relation = except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1)
case conditions
when Array, Hash
@@ -388,26 +371,31 @@ module ActiveRecord
relation
end
- def construct_join_dependency(joins = [], eager_loading: true)
+ def construct_join_dependency
including = eager_load_values + includes_values
- ActiveRecord::Associations::JoinDependency.new(klass, table, including, joins, eager_loading: eager_loading)
- end
-
- def construct_relation_for_association_calculations
- apply_join_dependency(self, construct_join_dependency(joins_values))
+ joins = joins_values.select { |join| join.is_a?(Arel::Nodes::Join) }
+ ActiveRecord::Associations::JoinDependency.new(
+ klass, table, including, alias_tracker(joins)
+ )
end
- def apply_join_dependency(relation, join_dependency)
- relation = relation.except(:includes, :eager_load, :preload).joins!(join_dependency)
+ def apply_join_dependency(eager_loading: true)
+ join_dependency = construct_join_dependency
+ relation = except(:includes, :eager_load, :preload).joins!(join_dependency)
- if using_limitable_reflections?(join_dependency.reflections)
- relation
- else
- if relation.limit_value
+ if eager_loading && !using_limitable_reflections?(join_dependency.reflections)
+ if has_limit_or_offset?
limited_ids = limited_ids_for(relation)
limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids)
end
- relation.except(:limit, :offset)
+ relation.limit_value = relation.offset_value = nil
+ end
+
+ if block_given?
+ relation._select!(join_dependency.aliases.columns)
+ yield relation, join_dependency
+ else
+ relation
end
end
@@ -435,9 +423,12 @@ module ActiveRecord
ids = ids.flatten.compact.uniq
+ model_name = @klass.name
+
case ids.size
when 0
- raise RecordNotFound, "Couldn't find #{@klass.name} without an ID"
+ error_message = "Couldn't find #{model_name} without an ID"
+ raise RecordNotFound.new(error_message, model_name, primary_key)
when 1
result = find_one(ids.first)
expects_array ? [ result ] : result
@@ -445,7 +436,8 @@ module ActiveRecord
find_some(ids)
end
rescue ::RangeError
- raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID"
+ error_message = "Couldn't find #{model_name} with an out of range ID"
+ raise RecordNotFound.new(error_message, model_name, primary_key, ids)
end
def find_one(id)
@@ -529,7 +521,11 @@ module ActiveRecord
else
relation = ordered_relation
- if limit_value.nil? || index < limit_value
+ if limit_value
+ limit = [limit_value - index, limit].min
+ end
+
+ if limit > 0
relation = relation.offset(offset_index + index) unless index.zero?
relation.limit(limit).to_a
else
@@ -544,12 +540,11 @@ module ActiveRecord
else
relation = ordered_relation
- relation.to_a[-index]
- # TODO: can be made more performant on large result sets by
- # for instance, last(index)[-index] (which would require
- # refactoring the last(n) finder method to make test suite pass),
- # or by using a combination of reverse_order, limit, and offset,
- # e.g., reverse_order.offset(index-1).first
+ if equal?(relation) || has_limit_or_offset?
+ relation.records[-index]
+ else
+ relation.last(index)[-index]
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 03824ffff9..25510d4a57 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -23,7 +23,11 @@ module ActiveRecord
# build a relation to merge in rather than directly merging
# the values.
def other
- other = Relation.create(relation.klass, relation.table, relation.predicate_builder)
+ other = Relation.create(
+ relation.klass,
+ table: relation.table,
+ predicate_builder: relation.predicate_builder
+ )
hash.each { |k, v|
if k == :joins
if Hash === v
@@ -52,7 +56,7 @@ module ActiveRecord
NORMAL_VALUES = Relation::VALUE_METHODS -
Relation::CLAUSE_METHODS -
- [:includes, :preload, :joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc:
+ [:includes, :preload, :joins, :left_outer_joins, :order, :reverse_order, :lock, :create_with, :reordering] # :nodoc:
def normal_values
NORMAL_VALUES
@@ -79,6 +83,7 @@ module ActiveRecord
merge_clauses
merge_preloads
merge_joins
+ merge_outer_joins
relation
end
@@ -112,22 +117,43 @@ module ActiveRecord
if other.klass == relation.klass
relation.joins!(*other.joins_values)
else
- joins_dependency, rest = other.joins_values.partition do |join|
+ alias_tracker = nil
+ joins_dependency = other.joins_values.map do |join|
case join
when Hash, Symbol, Array
- true
+ alias_tracker ||= other.alias_tracker
+ ActiveRecord::Associations::JoinDependency.new(
+ other.klass, other.table, join, alias_tracker
+ )
else
- false
+ join
end
end
- join_dependency = ActiveRecord::Associations::JoinDependency.new(
- other.klass, other.table, joins_dependency, []
- )
+ relation.joins!(*joins_dependency)
+ end
+ end
+
+ def merge_outer_joins
+ return if other.left_outer_joins_values.blank?
- relation.joins! rest
+ if other.klass == relation.klass
+ relation.left_outer_joins!(*other.left_outer_joins_values)
+ else
+ alias_tracker = nil
+ joins_dependency = other.left_outer_joins_values.map do |join|
+ case join
+ when Hash, Symbol, Array
+ alias_tracker ||= other.alias_tracker
+ ActiveRecord::Associations::JoinDependency.new(
+ other.klass, other.table, join, alias_tracker
+ )
+ else
+ join
+ end
+ end
- @relation = relation.joins join_dependency
+ relation.left_outer_joins!(*joins_dependency)
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 5c42414072..7a0edcbc33 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -13,6 +13,7 @@ module ActiveRecord
register_handler(Range, RangeHandler.new(self))
register_handler(Relation, RelationHandler.new)
register_handler(Array, ArrayHandler.new(self))
+ register_handler(Set, ArrayHandler.new(self))
end
def build_from_hash(attributes)
@@ -56,9 +57,6 @@ module ActiveRecord
end
protected
-
- attr_reader :table
-
def expand_from_hash(attributes)
return ["1=0"] if attributes.empty?
@@ -85,6 +83,18 @@ module ActiveRecord
expand_from_hash(query).reduce(&:and)
end
queries.reduce(&:or)
+ elsif table.aggregated_with?(key)
+ mapping = table.reflect_on_aggregation(key).mapping
+ queries = Array.wrap(value).map do |object|
+ mapping.map do |field_attr, aggregate_attr|
+ if mapping.size == 1 && !object.respond_to?(aggregate_attr)
+ build(table.arel_attribute(field_attr), object)
+ else
+ build(table.arel_attribute(field_attr), object.send(aggregate_attr))
+ end
+ end.reduce(&:and)
+ end
+ queries.reduce(&:or)
# FIXME: Deprecate this and provide a public API to force equality
elsif (value.is_a?(Range) || value.is_a?(Array)) &&
table.type(key.to_s).respond_to?(:subtype)
@@ -96,6 +106,7 @@ module ActiveRecord
end
private
+ attr_reader :table
def associated_predicate_builder(association_name)
self.class.new(table.associated_table(association_name))
@@ -123,11 +134,11 @@ module ActiveRecord
end
end
-require_relative "predicate_builder/array_handler"
-require_relative "predicate_builder/base_handler"
-require_relative "predicate_builder/basic_object_handler"
-require_relative "predicate_builder/range_handler"
-require_relative "predicate_builder/relation_handler"
+require "active_record/relation/predicate_builder/array_handler"
+require "active_record/relation/predicate_builder/base_handler"
+require "active_record/relation/predicate_builder/basic_object_handler"
+require "active_record/relation/predicate_builder/range_handler"
+require "active_record/relation/predicate_builder/relation_handler"
-require_relative "predicate_builder/association_query_value"
-require_relative "predicate_builder/polymorphic_array_value"
+require "active_record/relation/predicate_builder/association_query_value"
+require "active_record/relation/predicate_builder/polymorphic_array_value"
diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
index 2fd75c8958..64bf83e3c1 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -34,8 +34,7 @@ module ActiveRecord
array_predicates.inject(&:or)
end
- protected
-
+ private
attr_reader :predicate_builder
module NullPredicate # :nodoc:
diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb
index 0255a65bfe..88cd71cf69 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb
@@ -12,12 +12,9 @@ module ActiveRecord
[associated_table.association_join_foreign_key.to_s => ids]
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
+ private
attr_reader :associated_table, :value
- private
def ids
case value
when Relation
@@ -30,7 +27,7 @@ module ActiveRecord
end
def primary_key
- associated_table.association_join_keys.key
+ associated_table.association_join_primary_key
end
def convert_to_id(value)
diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
index 112821135f..10c5c1a66a 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb
@@ -11,8 +11,7 @@ module ActiveRecord
predicate_builder.build(attribute, value.id)
end
- protected
-
+ private
attr_reader :predicate_builder
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
index 34db266f05..e8c9f60860 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb
@@ -12,8 +12,7 @@ module ActiveRecord
attribute.eq(bind)
end
- protected
-
+ private
attr_reader :predicate_builder
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb
index b87b5c36dd..aae04d9348 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb
@@ -17,27 +17,26 @@ module ActiveRecord
end
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
+ private
attr_reader :associated_table, :values
- private
def type_to_ids_mapping
default_hash = Hash.new { |hsh, key| hsh[key] = [] }
- values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) }
+ values.each_with_object(default_hash) do |value, hash|
+ hash[klass(value).polymorphic_name] << convert_to_id(value)
+ end
end
def primary_key(value)
- associated_table.association_primary_key(base_class(value))
+ associated_table.association_join_primary_key(klass(value))
end
- def base_class(value)
+ def klass(value)
case value
when Base
- value.class.base_class
+ value.class
when Relation
- value.klass.base_class
+ value.klass
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
index 6d16579708..44bb2c7ab6 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb
@@ -16,15 +16,16 @@ module ActiveRecord
def call(attribute, value)
begin_bind = predicate_builder.build_bind_attribute(attribute.name, value.begin)
end_bind = predicate_builder.build_bind_attribute(attribute.name, value.end)
- if value.begin.respond_to?(:infinite?) && value.begin.infinite?
- if value.end.respond_to?(:infinite?) && value.end.infinite?
+
+ if begin_bind.value.infinity?
+ if end_bind.value.infinity?
attribute.not_in([])
elsif value.exclude_end?
attribute.lt(end_bind)
else
attribute.lteq(end_bind)
end
- elsif value.end.respond_to?(:infinite?) && value.end.infinite?
+ elsif end_bind.value.infinity?
attribute.gteq(begin_bind)
elsif value.exclude_end?
attribute.gteq(begin_bind).and(attribute.lt(end_bind))
@@ -33,8 +34,7 @@ module ActiveRecord
end
end
- protected
-
+ private
attr_reader :predicate_builder
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
index f51ea4fde0..c8bbfa5051 100644
--- a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
@@ -4,6 +4,10 @@ module ActiveRecord
class PredicateBuilder
class RelationHandler # :nodoc:
def call(attribute, value)
+ if value.eager_loading?
+ value = value.send(:apply_join_dependency)
+ end
+
if value.select_values.empty?
value = value.select(value.arel_attribute(value.klass.primary_key))
end
diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb
index 5a9a7fd432..f64bd30d38 100644
--- a/activerecord/lib/active_record/relation/query_attribute.rb
+++ b/activerecord/lib/active_record/relation/query_attribute.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
-require_relative "../attribute"
+require "active_model/attribute"
module ActiveRecord
class Relation
- class QueryAttribute < Attribute # :nodoc:
+ class QueryAttribute < ActiveModel::Attribute # :nodoc:
def type_cast(value)
value
end
@@ -21,6 +21,23 @@ module ActiveRecord
!value_before_type_cast.is_a?(StatementCache::Substitute) &&
(value_before_type_cast.nil? || value_for_database.nil?)
end
+
+ def boundable?
+ return @_boundable if defined?(@_boundable)
+ nil?
+ @_boundable = true
+ rescue ::RangeError
+ @_boundable = false
+ end
+
+ def infinity?
+ _infinity?(value_before_type_cast) || boundable? && _infinity?(value_for_database)
+ end
+
+ private
+ def _infinity?(value)
+ value.respond_to?(:infinite?) && value.infinite?
+ end
end
end
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index c88603fde2..db9101a168 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
-require_relative "from_clause"
-require_relative "query_attribute"
-require_relative "where_clause"
-require_relative "where_clause_factory"
+require "active_record/relation/from_clause"
+require "active_record/relation/query_attribute"
+require "active_record/relation/where_clause"
+require "active_record/relation/where_clause_factory"
require "active_model/forbidden_attributes_protection"
module ActiveRecord
@@ -231,6 +231,7 @@ module ActiveRecord
end
def _select!(*fields) # :nodoc:
+ fields.reject!(&:blank?)
fields.flatten!
fields.map! do |field|
klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field
@@ -295,6 +296,7 @@ module ActiveRecord
spawn.order!(*args)
end
+ # Same as #order but operates on relation in-place instead of copying.
def order!(*args) # :nodoc:
preprocess_order_args(args)
@@ -316,6 +318,7 @@ module ActiveRecord
spawn.reorder!(*args)
end
+ # Same as #reorder but operates on relation in-place instead of copying.
def reorder!(*args) # :nodoc:
preprocess_order_args(args)
@@ -325,8 +328,8 @@ module ActiveRecord
end
VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
- :limit, :offset, :joins, :includes, :from,
- :readonly, :having])
+ :limit, :offset, :joins, :left_outer_joins,
+ :includes, :from, :readonly, :having])
# Removes an unwanted relation that is already defined on a chain of relations.
# This is useful when passing around chains of relations and would like to
@@ -373,10 +376,11 @@ module ActiveRecord
args.each do |scope|
case scope
when Symbol
+ scope = :left_outer_joins if scope == :left_joins
if !VALID_UNSCOPING_VALUES.include?(scope)
raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
end
- set_value(scope, nil)
+ set_value(scope, DEFAULT_VALUES[scope])
when Hash
scope.each do |key, target_value|
if key != :where
@@ -441,16 +445,14 @@ module ActiveRecord
# => SELECT "users".* FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"
#
def left_outer_joins(*args)
- check_if_method_has_arguments!(:left_outer_joins, args)
-
- args.compact!
- args.flatten!
-
+ check_if_method_has_arguments!(__callee__, args)
spawn.left_outer_joins!(*args)
end
alias :left_joins :left_outer_joins
def left_outer_joins!(*args) # :nodoc:
+ args.compact!
+ args.flatten!
self.left_outer_joins_values += args
self
end
@@ -892,21 +894,27 @@ module ActiveRecord
self
end
- def skip_query_cache! # :nodoc:
- self.skip_query_cache_value = true
+ def skip_query_cache!(value = true) # :nodoc:
+ self.skip_query_cache_value = value
+ self
+ end
+
+ def skip_preloading! # :nodoc:
+ self.skip_preloading_value = true
self
end
# Returns the Arel object associated with the relation.
- def arel # :nodoc:
- @arel ||= build_arel
+ def arel(aliases = nil) # :nodoc:
+ @arel ||= build_arel(aliases)
+ end
+
+ # Returns a relation value with a given name
+ def get_value(name) # :nodoc:
+ @values.fetch(name, DEFAULT_VALUES[name])
end
protected
- # Returns a relation value with a given name
- def get_value(name) # :nodoc:
- @values[name] || default_value_for(name)
- end
# Sets the relation value with the given name
def set_value(name, value) # :nodoc:
@@ -921,16 +929,16 @@ module ActiveRecord
raise ImmutableRelation if defined?(@arel) && @arel
end
- def build_arel
+ def build_arel(aliases)
arel = Arel::SelectManager.new(table)
- build_joins(arel, joins_values.flatten) unless joins_values.empty?
- build_left_outer_joins(arel, left_outer_joins_values.flatten) unless left_outer_joins_values.empty?
+ aliases = build_joins(arel, joins_values.flatten, aliases) unless joins_values.empty?
+ build_left_outer_joins(arel, left_outer_joins_values.flatten, aliases) unless left_outer_joins_values.empty?
arel.where(where_clause.ast) unless where_clause.empty?
arel.having(having_clause.ast) unless having_clause.empty?
if limit_value
- limit_attribute = Attribute.with_cast_value(
+ limit_attribute = ActiveModel::Attribute.with_cast_value(
"LIMIT".freeze,
connection.sanitize_limit(limit_value),
Type.default_value,
@@ -938,7 +946,7 @@ module ActiveRecord
arel.take(Arel::Nodes::BindParam.new(limit_attribute))
end
if offset_value
- offset_attribute = Attribute.with_cast_value(
+ offset_attribute = ActiveModel::Attribute.with_cast_value(
"OFFSET".freeze,
offset_value.to_i,
Type.default_value,
@@ -963,6 +971,9 @@ module ActiveRecord
name = from_clause.name
case opts
when Relation
+ if opts.eager_loading?
+ opts = opts.send(:apply_join_dependency)
+ end
name ||= "subquery"
opts.arel.as(name.to_s)
else
@@ -970,20 +981,22 @@ module ActiveRecord
end
end
- def build_left_outer_joins(manager, outer_joins)
+ def build_left_outer_joins(manager, outer_joins, aliases)
buckets = outer_joins.group_by do |join|
case join
when Hash, Symbol, Array
:association_join
+ when ActiveRecord::Associations::JoinDependency
+ :stashed_join
else
raise ArgumentError, "only Hash, Symbol and Array are allowed"
end
end
- build_join_query(manager, buckets, Arel::Nodes::OuterJoin)
+ build_join_query(manager, buckets, Arel::Nodes::OuterJoin, aliases)
end
- def build_joins(manager, joins)
+ def build_joins(manager, joins, aliases)
buckets = joins.group_by do |join|
case join
when String
@@ -999,10 +1012,10 @@ module ActiveRecord
end
end
- build_join_query(manager, buckets, Arel::Nodes::InnerJoin)
+ build_join_query(manager, buckets, Arel::Nodes::InnerJoin, aliases)
end
- def build_join_query(manager, buckets, join_type)
+ def build_join_query(manager, buckets, join_type, aliases)
buckets.default = []
association_joins = buckets[:association_join]
@@ -1010,10 +1023,11 @@ module ActiveRecord
join_nodes = buckets[:join_node].uniq
string_joins = buckets[:string_join].map(&:strip).uniq
- join_list = join_nodes + convert_join_strings_to_ast(manager, string_joins)
+ join_list = join_nodes + convert_join_strings_to_ast(string_joins)
+ alias_tracker = alias_tracker(join_list, aliases)
join_dependency = ActiveRecord::Associations::JoinDependency.new(
- klass, table, association_joins, join_list
+ klass, table, association_joins, alias_tracker
)
joins = join_dependency.join_constraints(stashed_association_joins, join_type)
@@ -1021,10 +1035,10 @@ module ActiveRecord
manager.join_sources.concat(join_list)
- manager
+ alias_tracker.aliases
end
- def convert_join_strings_to_ast(table, joins)
+ def convert_join_strings_to_ast(joins)
joins
.flatten
.reject(&:blank?)
@@ -1034,6 +1048,8 @@ module ActiveRecord
def build_select(arel)
if select_values.any?
arel.project(*arel_columns(select_values.uniq))
+ elsif klass.ignored_columns.any?
+ arel.project(*klass.column_names.map { |field| arel_attribute(field) })
else
arel.project(table[Arel.star])
end
@@ -1070,7 +1086,7 @@ module ActiveRecord
end
o.split(",").map! do |s|
s.strip!
- s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || s.concat(" DESC")
+ s.gsub!(/\sasc\Z/i, " DESC") || s.gsub!(/\sdesc\Z/i, " ASC") || (s << " DESC")
end
else
o
@@ -1079,6 +1095,10 @@ module ActiveRecord
end
def does_not_support_reverse?(order)
+ # Account for String subclasses like Arel::Nodes::SqlLiteral that
+ # override methods like #count.
+ order = String.new(order) unless order.instance_of?(String)
+
# Uses SQL function with multiple arguments.
(order.include?(",") && order.split(",").find { |section| section.count("(") != section.count(")") }) ||
# Uses "nulls first" like construction.
@@ -1109,9 +1129,15 @@ module ActiveRecord
def preprocess_order_args(order_args)
order_args.map! do |arg|
- klass.send(:sanitize_sql_for_order, arg)
+ klass.sanitize_sql_for_order(arg)
end
order_args.flatten!
+
+ @klass.enforce_raw_sql_whitelist(
+ order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a },
+ whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST
+ )
+
validate_order_args(order_args)
references = order_args.grep(String)
@@ -1174,7 +1200,6 @@ module ActiveRecord
DEFAULT_VALUES = {
create_with: FROZEN_EMPTY_HASH,
- readonly: false,
where: Relation::WhereClause.empty,
having: Relation::WhereClause.empty,
from: Relation::FromClause.empty
@@ -1183,15 +1208,5 @@ module ActiveRecord
Relation::MULTI_VALUE_METHODS.each do |value|
DEFAULT_VALUES[value] ||= FROZEN_EMPTY_ARRAY
end
-
- Relation::SINGLE_VALUE_METHODS.each do |value|
- DEFAULT_VALUES[value] = nil if DEFAULT_VALUES[value].nil?
- end
-
- def default_value_for(name)
- DEFAULT_VALUES.fetch(name) do
- raise ArgumentError, "unknown relation value #{name.inspect}"
- end
- end
end
end
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 424894f835..b092399657 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -2,7 +2,7 @@
require "active_support/core_ext/hash/except"
require "active_support/core_ext/hash/slice"
-require_relative "merger"
+require "active_record/relation/merger"
module ActiveRecord
module SpawnMethods
@@ -10,6 +10,7 @@ module ActiveRecord
def spawn #:nodoc:
clone
end
+ alias :all :spawn
# Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation.
# Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array.
@@ -69,7 +70,7 @@ module ActiveRecord
private
def relation_with(values)
- result = Relation.create(klass, table, predicate_builder, values)
+ result = Relation.create(klass, values: values)
result.extend(*extending_values) if extending_values.any?
result
end
diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb
index 752bb38481..a502713e56 100644
--- a/activerecord/lib/active_record/relation/where_clause.rb
+++ b/activerecord/lib/active_record/relation/where_clause.rb
@@ -47,7 +47,7 @@ module ActiveRecord
end
def to_h(table_name = nil)
- equalities = predicates.grep(Arel::Nodes::Equality)
+ equalities = equalities(predicates)
if table_name
equalities = equalities.select do |node|
node.left.relation.name == table_name
@@ -90,6 +90,20 @@ module ActiveRecord
end
private
+ def equalities(predicates)
+ equalities = []
+
+ predicates.each do |node|
+ case node
+ when Arel::Nodes::Equality
+ equalities << node
+ when Arel::Nodes::And
+ equalities.concat equalities(node.children)
+ end
+ end
+
+ equalities
+ end
def predicates_unreferenced_by(other)
predicates.reject do |n|
@@ -121,7 +135,7 @@ module ActiveRecord
end
def except_predicates(columns)
- self.predicates.reject do |node|
+ predicates.reject do |node|
case node
when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual
subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right)
diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb
index 1374785354..c1b3eea9df 100644
--- a/activerecord/lib/active_record/relation/where_clause_factory.rb
+++ b/activerecord/lib/active_record/relation/where_clause_factory.rb
@@ -11,10 +11,9 @@ module ActiveRecord
def build(opts, other)
case opts
when String, Array
- parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))]
+ parts = [klass.sanitize_sql(other.empty? ? opts : ([opts] + other))]
when Hash
attributes = predicate_builder.resolve_column_aliases(opts)
- attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes)
attributes.stringify_keys!
parts = predicate_builder.build_from_hash(attributes)
@@ -27,8 +26,7 @@ module ActiveRecord
WhereClause.new(parts)
end
- protected
-
+ private
attr_reader :klass, :predicate_builder
end
end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 91a4f1fad6..c6c268855e 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -5,72 +5,135 @@ module ActiveRecord
extend ActiveSupport::Concern
module ClassMethods
- private
-
- # Accepts an array or string of SQL conditions and sanitizes
- # them into a valid SQL fragment for a WHERE clause.
- #
- # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
- # # => "name='foo''bar' and group_id=4"
- #
- # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
- # # => "name='foo''bar' and group_id='4'"
- #
- # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
- # # => "name='foo''bar' and group_id='4'"
- #
- # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
- # # => "name='foo''bar' and group_id='4'"
- def sanitize_sql_for_conditions(condition) # :doc:
- return nil if condition.blank?
-
- case condition
- when Array; sanitize_sql_array(condition)
- else condition
- end
+ # Accepts an array or string of SQL conditions and sanitizes
+ # them into a valid SQL fragment for a WHERE clause.
+ #
+ # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
+ # # => "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
+ # # => "name='foo''bar' and group_id='4'"
+ def sanitize_sql_for_conditions(condition)
+ return nil if condition.blank?
+
+ case condition
+ when Array; sanitize_sql_array(condition)
+ else condition
end
- alias :sanitize_sql :sanitize_sql_for_conditions
- alias :sanitize_conditions :sanitize_sql
- deprecate sanitize_conditions: :sanitize_sql
-
- # Accepts an array, hash, or string of SQL conditions and sanitizes
- # them into a valid SQL fragment for a SET clause.
- #
- # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
- # # => "name=NULL and group_id=4"
- #
- # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4])
- # # => "name=NULL and group_id=4"
- #
- # Post.send(:sanitize_sql_for_assignment, { name: nil, group_id: 4 })
- # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4"
- #
- # sanitize_sql_for_assignment("name=NULL and group_id='4'")
- # # => "name=NULL and group_id='4'"
- def sanitize_sql_for_assignment(assignments, default_table_name = table_name) # :doc:
- case assignments
- when Array; sanitize_sql_array(assignments)
- when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name)
- else assignments
- end
+ end
+ alias :sanitize_sql :sanitize_sql_for_conditions
+
+ # Accepts an array, hash, or string of SQL conditions and sanitizes
+ # them into a valid SQL fragment for a SET clause.
+ #
+ # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
+ # # => "name=NULL and group_id=4"
+ #
+ # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4])
+ # # => "name=NULL and group_id=4"
+ #
+ # Post.sanitize_sql_for_assignment({ name: nil, group_id: 4 })
+ # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4"
+ #
+ # sanitize_sql_for_assignment("name=NULL and group_id='4'")
+ # # => "name=NULL and group_id='4'"
+ def sanitize_sql_for_assignment(assignments, default_table_name = table_name)
+ case assignments
+ when Array; sanitize_sql_array(assignments)
+ when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name)
+ else assignments
end
-
- # Accepts an array, or string of SQL conditions and sanitizes
- # them into a valid SQL fragment for an ORDER clause.
- #
- # sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
- # # => "field(id, 1,3,2)"
- #
- # sanitize_sql_for_order("id ASC")
- # # => "id ASC"
- def sanitize_sql_for_order(condition) # :doc:
- if condition.is_a?(Array) && condition.first.to_s.include?("?")
- sanitize_sql_array(condition)
- else
- condition
+ end
+
+ # Accepts an array, or string of SQL conditions and sanitizes
+ # them into a valid SQL fragment for an ORDER clause.
+ #
+ # sanitize_sql_for_order(["field(id, ?)", [1,3,2]])
+ # # => "field(id, 1,3,2)"
+ #
+ # sanitize_sql_for_order("id ASC")
+ # # => "id ASC"
+ def sanitize_sql_for_order(condition)
+ if condition.is_a?(Array) && condition.first.to_s.include?("?")
+ enforce_raw_sql_whitelist([condition.first],
+ whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST
+ )
+
+ # Ensure we aren't dealing with a subclass of String that might
+ # override methods we use (eg. Arel::Nodes::SqlLiteral).
+ if condition.first.kind_of?(String) && !condition.first.instance_of?(String)
+ condition = [String.new(condition.first), *condition[1..-1]]
end
+
+ Arel.sql(sanitize_sql_array(condition))
+ else
+ condition
end
+ end
+
+ # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
+ #
+ # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
+ # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1"
+ def sanitize_sql_hash_for_assignment(attrs, table)
+ c = connection
+ attrs.map do |attr, value|
+ type = type_for_attribute(attr)
+ value = type.serialize(type.cast(value))
+ "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}"
+ end.join(", ")
+ end
+
+ # Sanitizes a +string+ so that it is safe to use within an SQL
+ # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%".
+ #
+ # sanitize_sql_like("100%")
+ # # => "100\\%"
+ #
+ # sanitize_sql_like("snake_cased_string")
+ # # => "snake\\_cased\\_string"
+ #
+ # sanitize_sql_like("100%", "!")
+ # # => "100!%"
+ #
+ # sanitize_sql_like("snake_cased_string", "!")
+ # # => "snake!_cased!_string"
+ def sanitize_sql_like(string, escape_character = "\\")
+ pattern = Regexp.union(escape_character, "%", "_")
+ string.gsub(pattern) { |x| [escape_character, x].join }
+ end
+
+ # Accepts an array of conditions. The array has each value
+ # sanitized and interpolated into the SQL statement.
+ #
+ # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id='4'"
+ def sanitize_sql_array(ary)
+ statement, *values = ary
+ if values.first.is_a?(Hash) && /:\w+/.match?(statement)
+ replace_named_bind_variables(statement, values.first)
+ elsif statement.include?("?")
+ replace_bind_variables(statement, values)
+ elsif statement.blank?
+ statement
+ else
+ statement % values.collect { |value| connection.quote_string(value.to_s) }
+ end
+ end
+ private
# Accepts a hash of SQL conditions and replaces those attributes
# that correspond to a {#composed_of}[rdoc-ref:Aggregations::ClassMethods#composed_of]
# relationship with their expanded aggregate attribute values.
@@ -92,10 +155,12 @@ module ActiveRecord
if aggregation = reflect_on_aggregation(attr.to_sym)
mapping = aggregation.mapping
mapping.each do |field_attr, aggregate_attr|
- if mapping.size == 1 && !value.respond_to?(aggregate_attr)
- expanded_attrs[field_attr] = value
+ expanded_attrs[field_attr] = if value.is_a?(Array)
+ value.map { |it| it.send(aggregate_attr) }
+ elsif mapping.size == 1 && !value.respond_to?(aggregate_attr)
+ value
else
- expanded_attrs[field_attr] = value.send(aggregate_attr)
+ value.send(aggregate_attr)
end
end
else
@@ -104,61 +169,7 @@ module ActiveRecord
end
expanded_attrs
end
-
- # Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
- #
- # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
- # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1"
- def sanitize_sql_hash_for_assignment(attrs, table) # :doc:
- c = connection
- attrs.map do |attr, value|
- value = type_for_attribute(attr.to_s).serialize(value)
- "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}"
- end.join(", ")
- end
-
- # Sanitizes a +string+ so that it is safe to use within an SQL
- # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%".
- #
- # sanitize_sql_like("100%")
- # # => "100\\%"
- #
- # sanitize_sql_like("snake_cased_string")
- # # => "snake\\_cased\\_string"
- #
- # sanitize_sql_like("100%", "!")
- # # => "100!%"
- #
- # sanitize_sql_like("snake_cased_string", "!")
- # # => "snake!_cased!_string"
- def sanitize_sql_like(string, escape_character = "\\") # :doc:
- pattern = Regexp.union(escape_character, "%", "_")
- string.gsub(pattern) { |x| [escape_character, x].join }
- end
-
- # Accepts an array of conditions. The array has each value
- # sanitized and interpolated into the SQL statement.
- #
- # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
- # # => "name='foo''bar' and group_id=4"
- #
- # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4])
- # # => "name='foo''bar' and group_id=4"
- #
- # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
- # # => "name='foo''bar' and group_id='4'"
- def sanitize_sql_array(ary) # :doc:
- statement, *values = ary
- if values.first.is_a?(Hash) && /:\w+/.match?(statement)
- replace_named_bind_variables(statement, values.first)
- elsif statement.include?("?")
- replace_bind_variables(statement, values)
- elsif statement.blank?
- statement
- else
- statement % values.collect { |value| connection.quote_string(value.to_s) }
- end
- end
+ deprecate :expand_hash_conditions_for_aggregates
def replace_bind_variables(statement, values)
raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size)
@@ -207,10 +218,5 @@ module ActiveRecord
end
end
end
-
- def quoted_id # :nodoc:
- self.class.connection.quote(@attributes[self.class.primary_key].value_for_database)
- end
- deprecate :quoted_id
end
end
diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb
index 1e121f2a09..216359867c 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -55,7 +55,7 @@ module ActiveRecord
end
ActiveRecord::InternalMetadata.create_table
- ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
+ ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment
end
private
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index 8d0311fabd..d475e77444 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -17,6 +17,12 @@ module ActiveRecord
# Only strings are accepted if ActiveRecord::Base.schema_format == :sql.
cattr_accessor :ignore_tables, default: []
+ ##
+ # :singleton-method:
+ # Specify a custom regular expression matching foreign keys which name
+ # should not be dumped to db/schema.rb.
+ cattr_accessor :fk_ignore_pattern, default: /^fk_rails_[0-9a-f]{10}$/
+
class << self
def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base)
connection.create_schema_dumper(generate_options(config)).dump(stream)
@@ -44,7 +50,7 @@ module ActiveRecord
def initialize(connection, options = {})
@connection = connection
- @version = Migrator::current_version rescue nil
+ @version = connection.migration_context.current_version rescue nil
@options = options
end
@@ -65,11 +71,11 @@ module ActiveRecord
# of editing this file, please use the migrations feature of Active Record to
# incrementally modify your database, and then regenerate this schema definition.
#
-# Note that this schema.rb definition is the authoritative source for your
-# database schema. If you need to create the application database on another
-# system, you should be using db:schema:load, not running all the migrations
-# from scratch. The latter is a flawed and unsustainable approach (the more migrations
-# you'll amass, the slower it'll run and the greater likelihood for issues).
+# This file is the source Rails uses to define your schema when running `rails
+# db:schema:load`. When creating a new database, `rails db:schema:load` tends to
+# be faster and is potentially less error prone than running all of your
+# migrations from scratch. Old migrations may fail to apply correctly if those
+# migrations use external dependencies or application code.
#
# It's strongly recommended that you check this file into your version control system.
@@ -82,16 +88,8 @@ HEADER
stream.puts "end"
end
+ # extensions are only supported by PostgreSQL
def extensions(stream)
- return unless @connection.supports_extensions?
- extensions = @connection.extensions
- if extensions.any?
- stream.puts " # These are extensions that must be enabled in order to support this database"
- extensions.sort.each do |extension|
- stream.puts " enable_extension #{extension.inspect}"
- end
- stream.puts
- end
end
def tables(stream)
@@ -192,8 +190,9 @@ HEADER
"name: #{index.name.inspect}",
]
index_parts << "unique: true" if index.unique
- index_parts << "length: { #{format_options(index.lengths)} }" if index.lengths.present?
- index_parts << "order: { #{format_options(index.orders)} }" if index.orders.present?
+ index_parts << "length: #{format_index_parts(index.lengths)}" if index.lengths.present?
+ index_parts << "order: #{format_index_parts(index.orders)}" if index.orders.present?
+ index_parts << "opclass: #{format_index_parts(index.opclasses)}" if index.opclasses.present?
index_parts << "where: #{index.where.inspect}" if index.where
index_parts << "using: #{index.using.inspect}" if !@connection.default_index_type?(index)
index_parts << "type: #{index.type.inspect}" if index.type
@@ -217,7 +216,7 @@ HEADER
parts << "primary_key: #{foreign_key.primary_key.inspect}"
end
- if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/
+ if foreign_key.export_name_on_schema_dump?
parts << "name: #{foreign_key.name.inspect}"
end
@@ -239,6 +238,14 @@ HEADER
options.map { |key, value| "#{key}: #{value.inspect}" }.join(", ")
end
+ def format_index_parts(options)
+ if options.is_a?(Hash)
+ "{ #{format_options(options)} }"
+ else
+ options.inspect
+ end
+ end
+
def remove_prefix_and_suffix(table)
prefix = Regexp.escape(@options[:table_name_prefix].to_s)
suffix = Regexp.escape(@options[:table_name_suffix].to_s)
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index 339a5334a8..f2d8b038fa 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "scoping/default"
-require_relative "scoping/named"
+require "active_record/scoping/default"
+require "active_record/scoping/named"
module ActiveRecord
# This class is used to create a table that keeps track of which migrations
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index da585a9562..01ac56570a 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -11,23 +11,23 @@ module ActiveRecord
include Named
end
- module ClassMethods
- def current_scope(skip_inherited_scope = false) # :nodoc:
+ module ClassMethods # :nodoc:
+ def current_scope(skip_inherited_scope = false)
ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope)
end
- def current_scope=(scope) #:nodoc:
+ def current_scope=(scope)
ScopeRegistry.set_value_for(:current_scope, self, scope)
end
# Collects attributes from scopes that should be applied when creating
# an AR instance for the particular class this is called on.
- def scope_attributes # :nodoc:
+ def scope_attributes
all.scope_for_create
end
# Are there attributes associated with this scope?
- def scope_attributes? # :nodoc:
+ def scope_attributes?
current_scope
end
end
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 86ae374318..8c612df27a 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -111,7 +111,7 @@ module ActiveRecord
# The user has defined their own default scope method, so call that
evaluate_default_scope do
if scope = default_scope
- (base_rel ||= relation).merge(scope)
+ (base_rel ||= relation).merge!(scope)
end
end
elsif default_scopes.any?
@@ -119,7 +119,7 @@ module ActiveRecord
evaluate_default_scope do
default_scopes.inject(base_rel) do |default_scope, scope|
scope = scope.respond_to?(:to_proc) ? scope : scope.method(:call)
- default_scope.merge(base_rel.instance_exec(&scope))
+ default_scope.merge!(base_rel.instance_exec(&scope))
end
end
end
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index 6fa096c1fe..a784001587 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -24,8 +24,14 @@ module ActiveRecord
# You can define a scope that applies to all finders using
# {default_scope}[rdoc-ref:Scoping::Default::ClassMethods#default_scope].
def all
+ current_scope = self.current_scope
+
if current_scope
- current_scope.clone
+ if self == current_scope.klass
+ current_scope.clone
+ else
+ relation.merge!(current_scope)
+ end
else
default_scoped
end
@@ -165,13 +171,19 @@ module ActiveRecord
"a class method with the same name."
end
+ if method_defined_within?(name, Relation)
+ raise ArgumentError, "You tried to define a scope named \"#{name}\" " \
+ "on the model \"#{self.name}\", but ActiveRecord::Relation already defined " \
+ "an instance method with the same name."
+ end
+
valid_scope_name?(name)
extension = Module.new(&block) if block
if body.respond_to?(:to_proc)
singleton_class.send(:define_method, name) do |*args|
scope = all
- scope = scope.instance_exec(*args, &body) || scope
+ scope = scope._exec_scope(*args, &body)
scope = scope.extending(extension) if extension
scope
end
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
index 59acd63a0f..b41d3504fd 100644
--- a/activerecord/lib/active_record/statement_cache.rb
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -114,8 +114,7 @@ module ActiveRecord
end
end
- protected
-
+ private
attr_reader :query_builder, :bind_map, :klass
end
end
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index 202b82fa61..8d628359c3 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -17,8 +17,8 @@ module ActiveRecord
# You can set custom coder to encode/decode your serialized attributes to/from different formats.
# JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+.
#
- # NOTE: If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for
- # the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store].
+ # NOTE: If you are using structured database data types (eg. PostgreSQL +hstore+/+json+, or MySQL 5.7+
+ # +json+) there is no need for the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store].
# Simply use {.store_accessor}[rdoc-ref:ClassMethods#store_accessor] instead to generate
# the accessor methods. Be aware that these columns use a string keyed hash and do not allow access
# using a symbol.
@@ -31,10 +31,14 @@ module ActiveRecord
#
# class User < ActiveRecord::Base
# store :settings, accessors: [ :color, :homepage ], coder: JSON
+ # store :parent, accessors: [ :name ], coder: JSON, prefix: true
+ # store :spouse, accessors: [ :name ], coder: JSON, prefix: :partner
# end
#
- # u = User.new(color: 'black', homepage: '37signals.com')
+ # u = User.new(color: 'black', homepage: '37signals.com', parent_name: 'Mary', partner_name: 'Lily')
# u.color # Accessor stored attribute
+ # u.parent_name # Accessor stored attribute with prefix
+ # u.partner_name # Accessor stored attribute with custom prefix
# u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor
#
# # There is no difference between strings and symbols for accessing custom attributes
@@ -44,6 +48,7 @@ module ActiveRecord
# # Add additional accessors to an existing store through store_accessor
# class SuperUser < User
# store_accessor :settings, :privileges, :servants
+ # store_accessor :parent, :birthday, prefix: true
# end
#
# The stored attribute names can be retrieved using {.stored_attributes}[rdoc-ref:rdoc-ref:ClassMethods#stored_attributes].
@@ -81,19 +86,29 @@ module ActiveRecord
module ClassMethods
def store(store_attribute, options = {})
serialize store_attribute, IndifferentCoder.new(store_attribute, options[:coder])
- store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors
+ store_accessor(store_attribute, options[:accessors], prefix: options[:prefix]) if options.has_key? :accessors
end
- def store_accessor(store_attribute, *keys)
+ def store_accessor(store_attribute, *keys, prefix: nil)
keys = keys.flatten
+ accessor_prefix =
+ case prefix
+ when String, Symbol
+ "#{prefix}_"
+ when TrueClass
+ "#{store_attribute}_"
+ else
+ ""
+ end
+
_store_accessors_module.module_eval do
keys.each do |key|
- define_method("#{key}=") do |value|
+ define_method("#{accessor_prefix}#{key}=") do |value|
write_store_attribute(store_attribute, key, value)
end
- define_method(key) do
+ define_method("#{accessor_prefix}#{key}") do
read_store_attribute(store_attribute, key)
end
end
@@ -135,7 +150,7 @@ module ActiveRecord
end
def store_accessor_for(store_attribute)
- type_for_attribute(store_attribute.to_s).accessor
+ type_for_attribute(store_attribute).accessor
end
class HashAccessor # :nodoc:
diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb
index a5187efc84..b67479fb6a 100644
--- a/activerecord/lib/active_record/table_metadata.rb
+++ b/activerecord/lib/active_record/table_metadata.rb
@@ -2,8 +2,7 @@
module ActiveRecord
class TableMetadata # :nodoc:
- delegate :foreign_type, :foreign_key, :join_keys, :join_foreign_key, to: :association, prefix: true
- delegate :association_primary_key, to: :association
+ delegate :foreign_type, :foreign_key, :join_primary_key, :join_foreign_key, to: :association, prefix: true
def initialize(klass, arel_table, association = nil)
@klass = klass
@@ -31,7 +30,7 @@ module ActiveRecord
def type(column_name)
if klass
- klass.type_for_attribute(column_name.to_s)
+ klass.type_for_attribute(column_name)
else
Type.default_value
end
@@ -66,10 +65,15 @@ module ActiveRecord
association && association.polymorphic?
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
+ def aggregated_with?(aggregation_name)
+ klass && reflect_on_aggregation(aggregation_name)
+ end
+
+ def reflect_on_aggregation(aggregation_name)
+ klass.reflect_on_aggregation(aggregation_name)
+ end
+ private
attr_reader :klass, :arel_table, :association
end
end
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index 0f3f84ca08..521375954b 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -54,10 +54,10 @@ module ActiveRecord
def check_protected_environments!
unless ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"]
- current = ActiveRecord::Migrator.current_environment
- stored = ActiveRecord::Migrator.last_stored_environment
+ current = ActiveRecord::Base.connection.migration_context.current_environment
+ stored = ActiveRecord::Base.connection.migration_context.last_stored_environment
- if ActiveRecord::Migrator.protected_environment?
+ if ActiveRecord::Base.connection.migration_context.protected_environment?
raise ActiveRecord::ProtectedEnvironmentError.new(stored)
end
@@ -117,9 +117,9 @@ module ActiveRecord
def create(*arguments)
configuration = arguments.first
class_for_adapter(configuration["adapter"]).new(*arguments).create
- $stdout.puts "Created database '#{configuration['database']}'"
+ $stdout.puts "Created database '#{configuration['database']}'" if verbose?
rescue DatabaseAlreadyExists
- $stderr.puts "Database '#{configuration['database']}' already exists"
+ $stderr.puts "Database '#{configuration['database']}' already exists" if verbose?
rescue Exception => error
$stderr.puts error
$stderr.puts "Couldn't create database for #{configuration.inspect}"
@@ -134,6 +134,18 @@ module ActiveRecord
end
end
+ def for_each
+ databases = Rails.application.config.load_database_yaml
+ database_configs = ActiveRecord::DatabaseConfigurations.configs_for(Rails.env, databases)
+
+ # if this is a single database application we don't want tasks for each primary database
+ return if database_configs.count == 1
+
+ database_configs.each do |db_config|
+ yield db_config.spec_name
+ end
+ end
+
def create_current(environment = env)
each_current_configuration(environment) { |configuration|
create configuration
@@ -144,7 +156,7 @@ module ActiveRecord
def drop(*arguments)
configuration = arguments.first
class_for_adapter(configuration["adapter"]).new(*arguments).drop
- $stdout.puts "Dropped database '#{configuration['database']}'"
+ $stdout.puts "Dropped database '#{configuration['database']}'" if verbose?
rescue ActiveRecord::NoDatabaseError
$stderr.puts "Database '#{configuration['database']}' does not exist"
rescue Exception => error
@@ -164,13 +176,11 @@ module ActiveRecord
end
def migrate
- raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty?
+ check_target_version
- verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true
- version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
scope = ENV["SCOPE"]
- verbose_was, Migration.verbose = Migration.verbose, verbose
- Migrator.migrate(migrations_paths, version) do |migration|
+ verbose_was, Migration.verbose = Migration.verbose, verbose?
+ Base.connection.migration_context.migrate(target_version) do |migration|
scope.blank? || scope == migration.scope
end
ActiveRecord::Base.clear_cache!
@@ -178,6 +188,16 @@ module ActiveRecord
Migration.verbose = verbose_was
end
+ def check_target_version
+ if target_version && !(Migration::MigrationFilenameRegexp.match?(ENV["VERSION"]) || /\A\d+\z/.match?(ENV["VERSION"]))
+ raise "Invalid format of target version: `VERSION=#{ENV['VERSION']}`"
+ end
+ end
+
+ def target_version
+ ENV["VERSION"].to_i if ENV["VERSION"] && !ENV["VERSION"].empty?
+ end
+
def charset_current(environment = env)
charset ActiveRecord::Base.configurations[environment]
end
@@ -225,36 +245,50 @@ module ActiveRecord
class_for_adapter(configuration["adapter"]).new(*arguments).structure_load(filename, structure_load_flags)
end
- def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc:
- file ||= schema_file(format)
+ def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env, spec_name = "primary") # :nodoc:
+ file ||= dump_filename(spec_name, format)
+
+ check_schema_file(file)
+ ActiveRecord::Base.establish_connection(configuration)
case format
when :ruby
- check_schema_file(file)
- ActiveRecord::Base.establish_connection(configuration)
load(file)
when :sql
- check_schema_file(file)
structure_load(configuration, file)
else
raise ArgumentError, "unknown format #{format.inspect}"
end
ActiveRecord::InternalMetadata.create_table
- ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
+ ActiveRecord::InternalMetadata[:environment] = environment
end
def schema_file(format = ActiveRecord::Base.schema_format)
+ File.join(db_dir, schema_file_type(format))
+ end
+
+ def schema_file_type(format = ActiveRecord::Base.schema_format)
case format
when :ruby
- File.join(db_dir, "schema.rb")
+ "schema.rb"
when :sql
- File.join(db_dir, "structure.sql")
+ "structure.sql"
+ end
+ end
+
+ def dump_filename(namespace, format = ActiveRecord::Base.schema_format)
+ filename = if namespace == "primary"
+ schema_file_type(format)
+ else
+ "#{namespace}_#{schema_file_type(format)}"
end
+
+ ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename)
end
def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env)
- each_current_configuration(environment) { |configuration|
- load_schema configuration, format, file
+ each_current_configuration(environment) { |configuration, spec_name, env|
+ load_schema(configuration, format, file, env, spec_name)
}
ActiveRecord::Base.establish_connection(environment.to_sym)
end
@@ -288,6 +322,9 @@ module ActiveRecord
end
private
+ def verbose?
+ ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true
+ end
def class_for_adapter(adapter)
_key, task = @tasks.each_pair.detect { |pattern, _task| adapter[pattern] }
@@ -301,9 +338,10 @@ module ActiveRecord
environments = [environment]
environments << "test" if environment == "development"
- configurations = ActiveRecord::Base.configurations.values_at(*environments)
- configurations.compact.each do |configuration|
- yield configuration unless configuration["database"].blank?
+ environments.each do |env|
+ ActiveRecord::DatabaseConfigurations.configs_for(env) do |spec_name, configuration|
+ yield configuration, spec_name, env
+ end
end
end
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index 84265aa9e3..e697fa6def 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -3,8 +3,6 @@
module ActiveRecord
module Tasks # :nodoc:
class MySQLDatabaseTasks # :nodoc:
- ACCESS_DENIED_ERROR = 1045
-
delegate :connection, :establish_connection, to: ActiveRecord::Base
def initialize(configuration)
@@ -21,20 +19,6 @@ module ActiveRecord
else
raise
end
- rescue error_class => error
- if error.respond_to?(:errno) && error.errno == ACCESS_DENIED_ERROR
- $stdout.print error.message
- establish_connection root_configuration_without_database
- connection.create_database configuration["database"], creation_options
- if configuration["username"] != "root"
- connection.execute grant_statement.gsub(/\s+/, " ").strip
- end
- establish_connection configuration
- else
- $stderr.puts error.inspect
- $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}"
- $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration["encoding"]
- end
end
def drop
@@ -99,37 +83,6 @@ module ActiveRecord
end
end
- def error_class
- if configuration["adapter"].include?("jdbc")
- require_relative "../railties/jdbcmysql_error"
- ArJdbcMySQL::Error
- elsif defined?(Mysql2)
- Mysql2::Error
- else
- StandardError
- end
- end
-
- def grant_statement
- <<-SQL
-GRANT ALL PRIVILEGES ON `#{configuration['database']}`.*
- TO '#{configuration['username']}'@'localhost'
-IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
- SQL
- end
-
- def root_configuration_without_database
- configuration_without_database.merge(
- "username" => "root",
- "password" => root_password
- )
- end
-
- def root_password
- $stdout.print "Please provide the root password for your MySQL installation\n>"
- $stdin.gets.strip
- end
-
def prepare_command_options
args = {
"host" => "--host",
diff --git a/activerecord/lib/active_record/test_databases.rb b/activerecord/lib/active_record/test_databases.rb
new file mode 100644
index 0000000000..606a3b0fb5
--- /dev/null
+++ b/activerecord/lib/active_record/test_databases.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require "active_support/testing/parallelization"
+
+module ActiveRecord
+ module TestDatabases # :nodoc:
+ ActiveSupport::Testing::Parallelization.after_fork_hook do |i|
+ create_and_migrate(i, spec_name: Rails.env)
+ end
+
+ ActiveSupport::Testing::Parallelization.run_cleanup_hook do |i|
+ drop(i, spec_name: Rails.env)
+ end
+
+ def self.create_and_migrate(i, spec_name:)
+ old, ENV["VERBOSE"] = ENV["VERBOSE"], "false"
+
+ connection_spec = ActiveRecord::Base.configurations[spec_name]
+
+ connection_spec["database"] += "-#{i}"
+ ActiveRecord::Tasks::DatabaseTasks.create(connection_spec)
+ ActiveRecord::Base.establish_connection(connection_spec)
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ ensure
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env])
+ ENV["VERBOSE"] = old
+ end
+
+ def self.drop(i, spec_name:)
+ old, ENV["VERBOSE"] = ENV["VERBOSE"], "false"
+ connection_spec = ActiveRecord::Base.configurations[spec_name]
+
+ ActiveRecord::Tasks::DatabaseTasks.drop(connection_spec)
+ ensure
+ ENV["VERBOSE"] = old
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index 5da3759e5a..e47f06bf3a 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -52,16 +52,20 @@ module ActiveRecord
clear_timestamp_attributes
end
- class_methods do
+ module ClassMethods # :nodoc:
+ def timestamp_attributes_for_update_in_model
+ timestamp_attributes_for_update.select { |c| column_names.include?(c) }
+ end
+
+ def current_time_from_proper_timezone
+ default_timezone == :utc ? Time.now.utc : Time.now
+ end
+
private
def timestamp_attributes_for_create_in_model
timestamp_attributes_for_create.select { |c| column_names.include?(c) }
end
- def timestamp_attributes_for_update_in_model
- timestamp_attributes_for_update.select { |c| column_names.include?(c) }
- end
-
def all_timestamp_attributes_in_model
timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model
end
@@ -73,10 +77,6 @@ module ActiveRecord
def timestamp_attributes_for_update
["updated_at", "updated_on"]
end
-
- def current_time_from_proper_timezone
- default_timezone == :utc ? Time.now.utc : Time.now
- end
end
private
@@ -116,7 +116,7 @@ module ActiveRecord
end
def timestamp_attributes_for_update_in_model
- self.class.send(:timestamp_attributes_for_update_in_model)
+ self.class.timestamp_attributes_for_update_in_model
end
def all_timestamp_attributes_in_model
@@ -124,7 +124,7 @@ module ActiveRecord
end
def current_time_from_proper_timezone
- self.class.send(:current_time_from_proper_timezone)
+ self.class.current_time_from_proper_timezone
end
def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update_in_model)
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 6761f2da25..97cba5d1c7 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -285,7 +285,7 @@ module ActiveRecord
fire_on = Array(options[:on])
assert_valid_transaction_action(fire_on)
options[:if] = Array(options[:if])
- options[:if].unshift("transaction_include_any_action?(#{fire_on})")
+ options[:if].unshift(-> { transaction_include_any_action?(fire_on) })
end
end
diff --git a/activerecord/lib/active_record/translation.rb b/activerecord/lib/active_record/translation.rb
index 3cf70eafb8..82661a328a 100644
--- a/activerecord/lib/active_record/translation.rb
+++ b/activerecord/lib/active_record/translation.rb
@@ -10,7 +10,7 @@ module ActiveRecord
classes = [klass]
return classes if klass == ActiveRecord::Base
- while klass != klass.base_class
+ while !klass.base_class?
classes << klass = klass.superclass
end
classes
diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb
index fa22df92b8..c303186ef2 100644
--- a/activerecord/lib/active_record/type.rb
+++ b/activerecord/lib/active_record/type.rb
@@ -2,21 +2,21 @@
require "active_model/type"
-require_relative "type/internal/timezone"
+require "active_record/type/internal/timezone"
-require_relative "type/date"
-require_relative "type/date_time"
-require_relative "type/decimal_without_scale"
-require_relative "type/json"
-require_relative "type/time"
-require_relative "type/text"
-require_relative "type/unsigned_integer"
+require "active_record/type/date"
+require "active_record/type/date_time"
+require "active_record/type/decimal_without_scale"
+require "active_record/type/json"
+require "active_record/type/time"
+require "active_record/type/text"
+require "active_record/type/unsigned_integer"
-require_relative "type/serialized"
-require_relative "type/adapter_specific_registry"
+require "active_record/type/serialized"
+require "active_record/type/adapter_specific_registry"
-require_relative "type/type_map"
-require_relative "type/hash_lookup_type_map"
+require "active_record/type/type_map"
+require "active_record/type/hash_lookup_type_map"
module ActiveRecord
module Type
diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb
index e7468aa542..b300fdfa05 100644
--- a/activerecord/lib/active_record/type/adapter_specific_registry.rb
+++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb
@@ -52,8 +52,6 @@ module ActiveRecord
priority <=> other.priority
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
protected
attr_reader :name, :block, :adapter, :override
@@ -114,13 +112,8 @@ module ActiveRecord
super | 4
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
- attr_reader :options, :klass
-
private
+ attr_reader :options, :klass
def matches_options?(**kwargs)
options.all? do |key, value|
diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb
index ed2e4fb79c..2e5f45fa3d 100644
--- a/activerecord/lib/active_record/type_caster.rb
+++ b/activerecord/lib/active_record/type_caster.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
-require_relative "type_caster/map"
-require_relative "type_caster/connection"
+require "active_record/type_caster/map"
+require "active_record/type_caster/connection"
module ActiveRecord
module TypeCaster # :nodoc:
diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb
index af4e4e37e2..7cf8181d8e 100644
--- a/activerecord/lib/active_record/type_caster/connection.rb
+++ b/activerecord/lib/active_record/type_caster/connection.rb
@@ -14,15 +14,10 @@ module ActiveRecord
connection.type_cast_from_column(column, value)
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :table_name
delegate :connection, to: :@klass
- private
-
def column_for(attribute_name)
if connection.schema_cache.data_source_exists?(table_name)
connection.schema_cache.columns_hash(table_name)[attribute_name.to_s]
diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb
index d51350ba83..663cdadb03 100644
--- a/activerecord/lib/active_record/type_caster/map.rb
+++ b/activerecord/lib/active_record/type_caster/map.rb
@@ -9,14 +9,11 @@ module ActiveRecord
def type_cast_for_database(attr_name, value)
return value if value.is_a?(Arel::Nodes::BindParam)
- type = types.type_for_attribute(attr_name.to_s)
+ type = types.type_for_attribute(attr_name)
type.serialize(value)
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
-
+ private
attr_reader :types
end
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 3f5c879f2f..ca27a3f0ab 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -86,8 +86,8 @@ module ActiveRecord
end
end
-require_relative "validations/associated"
-require_relative "validations/uniqueness"
-require_relative "validations/presence"
-require_relative "validations/absence"
-require_relative "validations/length"
+require "active_record/validations/associated"
+require "active_record/validations/uniqueness"
+require "active_record/validations/presence"
+require "active_record/validations/absence"
+require "active_record/validations/length"
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index f4ad58c087..5a1dbc8e53 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -23,7 +23,7 @@ module ActiveRecord
relation = build_relation(finder_class, attribute, value)
if record.persisted?
if finder_class.primary_key
- relation = relation.where.not(finder_class.primary_key => record.id_in_database || record.id)
+ relation = relation.where.not(finder_class.primary_key => record.id_in_database)
else
raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.")
end
@@ -205,9 +205,7 @@ module ActiveRecord
# | # Boom! We now have a duplicate
# | # title!
#
- # This could even happen if you use transactions with the 'serializable'
- # isolation level. The best way to work around this problem is to add a unique
- # index to the database table using
+ # The best way to work around this problem is to add a unique index to the database table using
# {connection.add_index}[rdoc-ref:ConnectionAdapters::SchemaStatements#add_index].
# In the rare case that a race condition occurs, the database will guarantee
# the field's uniqueness.
diff --git a/activerecord/lib/arel.rb b/activerecord/lib/arel.rb
new file mode 100644
index 0000000000..7d04e1cac6
--- /dev/null
+++ b/activerecord/lib/arel.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require "arel/errors"
+
+require "arel/crud"
+require "arel/factory_methods"
+
+require "arel/expressions"
+require "arel/predications"
+require "arel/window_predications"
+require "arel/math"
+require "arel/alias_predication"
+require "arel/order_predications"
+require "arel/table"
+require "arel/attributes"
+require "arel/compatibility/wheres"
+
+require "arel/visitors"
+require "arel/collectors/sql_string"
+
+require "arel/tree_manager"
+require "arel/insert_manager"
+require "arel/select_manager"
+require "arel/update_manager"
+require "arel/delete_manager"
+require "arel/nodes"
+
+module Arel # :nodoc: all
+ VERSION = "10.0.0"
+
+ def self.sql(raw_sql)
+ Arel::Nodes::SqlLiteral.new raw_sql
+ end
+
+ def self.star
+ sql "*"
+ end
+ ## Convenience Alias
+ Node = Arel::Nodes::Node
+end
diff --git a/activerecord/lib/arel/alias_predication.rb b/activerecord/lib/arel/alias_predication.rb
new file mode 100644
index 0000000000..4abbbb7ef6
--- /dev/null
+++ b/activerecord/lib/arel/alias_predication.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module AliasPredication
+ def as(other)
+ Nodes::As.new self, Nodes::SqlLiteral.new(other)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/attributes.rb b/activerecord/lib/arel/attributes.rb
new file mode 100644
index 0000000000..35d586c948
--- /dev/null
+++ b/activerecord/lib/arel/attributes.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require "arel/attributes/attribute"
+
+module Arel # :nodoc: all
+ module Attributes
+ ###
+ # Factory method to wrap a raw database +column+ to an Arel Attribute.
+ def self.for(column)
+ case column.type
+ when :string, :text, :binary then String
+ when :integer then Integer
+ when :float then Float
+ when :decimal then Decimal
+ when :date, :datetime, :timestamp, :time then Time
+ when :boolean then Boolean
+ else
+ Undefined
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/attributes/attribute.rb b/activerecord/lib/arel/attributes/attribute.rb
new file mode 100644
index 0000000000..ecf499a23e
--- /dev/null
+++ b/activerecord/lib/arel/attributes/attribute.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Attributes
+ class Attribute < Struct.new :relation, :name
+ include Arel::Expressions
+ include Arel::Predications
+ include Arel::AliasPredication
+ include Arel::OrderPredications
+ include Arel::Math
+
+ ###
+ # Create a node for lowering this attribute
+ def lower
+ relation.lower self
+ end
+
+ def type_cast_for_database(value)
+ relation.type_cast_for_database(name, value)
+ end
+
+ def able_to_type_cast?
+ relation.able_to_type_cast?
+ end
+ end
+
+ class String < Attribute; end
+ class Time < Attribute; end
+ class Boolean < Attribute; end
+ class Decimal < Attribute; end
+ class Float < Attribute; end
+ class Integer < Attribute; end
+ class Undefined < Attribute; end
+ end
+
+ Attribute = Attributes::Attribute
+end
diff --git a/activerecord/lib/arel/collectors/bind.rb b/activerecord/lib/arel/collectors/bind.rb
new file mode 100644
index 0000000000..6f8912575d
--- /dev/null
+++ b/activerecord/lib/arel/collectors/bind.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Collectors
+ class Bind
+ def initialize
+ @binds = []
+ end
+
+ def <<(str)
+ self
+ end
+
+ def add_bind(bind)
+ @binds << bind
+ self
+ end
+
+ def value
+ @binds
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/collectors/composite.rb b/activerecord/lib/arel/collectors/composite.rb
new file mode 100644
index 0000000000..d040d8598d
--- /dev/null
+++ b/activerecord/lib/arel/collectors/composite.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Collectors
+ class Composite
+ def initialize(left, right)
+ @left = left
+ @right = right
+ end
+
+ def <<(str)
+ left << str
+ right << str
+ self
+ end
+
+ def add_bind(bind, &block)
+ left.add_bind bind, &block
+ right.add_bind bind, &block
+ self
+ end
+
+ def value
+ [left.value, right.value]
+ end
+
+ protected
+
+ attr_reader :left, :right
+ end
+ end
+end
diff --git a/activerecord/lib/arel/collectors/plain_string.rb b/activerecord/lib/arel/collectors/plain_string.rb
new file mode 100644
index 0000000000..687d7fbf2f
--- /dev/null
+++ b/activerecord/lib/arel/collectors/plain_string.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Collectors
+ class PlainString
+ def initialize
+ @str = "".dup
+ end
+
+ def value
+ @str
+ end
+
+ def <<(str)
+ @str << str
+ self
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/collectors/sql_string.rb b/activerecord/lib/arel/collectors/sql_string.rb
new file mode 100644
index 0000000000..c293a89a74
--- /dev/null
+++ b/activerecord/lib/arel/collectors/sql_string.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require "arel/collectors/plain_string"
+
+module Arel # :nodoc: all
+ module Collectors
+ class SQLString < PlainString
+ def initialize(*)
+ super
+ @bind_index = 1
+ end
+
+ def add_bind(bind)
+ self << yield(@bind_index)
+ @bind_index += 1
+ self
+ end
+
+ def compile(bvs)
+ value
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/collectors/substitute_binds.rb b/activerecord/lib/arel/collectors/substitute_binds.rb
new file mode 100644
index 0000000000..3f40eec8a8
--- /dev/null
+++ b/activerecord/lib/arel/collectors/substitute_binds.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Collectors
+ class SubstituteBinds
+ def initialize(quoter, delegate_collector)
+ @quoter = quoter
+ @delegate = delegate_collector
+ end
+
+ def <<(str)
+ delegate << str
+ self
+ end
+
+ def add_bind(bind)
+ self << quoter.quote(bind)
+ end
+
+ def value
+ delegate.value
+ end
+
+ protected
+
+ attr_reader :quoter, :delegate
+ end
+ end
+end
diff --git a/activerecord/lib/arel/compatibility/wheres.rb b/activerecord/lib/arel/compatibility/wheres.rb
new file mode 100644
index 0000000000..c8a73f0dae
--- /dev/null
+++ b/activerecord/lib/arel/compatibility/wheres.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Compatibility # :nodoc:
+ class Wheres # :nodoc:
+ include Enumerable
+
+ module Value # :nodoc:
+ attr_accessor :visitor
+ def value
+ visitor.accept self
+ end
+
+ def name
+ super.to_sym
+ end
+ end
+
+ def initialize(engine, collection)
+ @engine = engine
+ @collection = collection
+ end
+
+ def each
+ to_sql = Visitors::ToSql.new @engine
+
+ @collection.each { |c|
+ c.extend(Value)
+ c.visitor = to_sql
+ yield c
+ }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/crud.rb b/activerecord/lib/arel/crud.rb
new file mode 100644
index 0000000000..e8a563ca4a
--- /dev/null
+++ b/activerecord/lib/arel/crud.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ ###
+ # FIXME hopefully we can remove this
+ module Crud
+ def compile_update(values, pk)
+ um = UpdateManager.new
+
+ if Nodes::SqlLiteral === values
+ relation = @ctx.from
+ else
+ relation = values.first.first.relation
+ end
+ um.key = pk
+ um.table relation
+ um.set values
+ um.take @ast.limit.expr if @ast.limit
+ um.order(*@ast.orders)
+ um.wheres = @ctx.wheres
+ um
+ end
+
+ def compile_insert(values)
+ im = create_insert
+ im.insert values
+ im
+ end
+
+ def create_insert
+ InsertManager.new
+ end
+
+ def compile_delete
+ dm = DeleteManager.new
+ dm.take @ast.limit.expr if @ast.limit
+ dm.wheres = @ctx.wheres
+ dm.from @ctx.froms
+ dm
+ end
+ end
+end
diff --git a/activerecord/lib/arel/delete_manager.rb b/activerecord/lib/arel/delete_manager.rb
new file mode 100644
index 0000000000..2def581009
--- /dev/null
+++ b/activerecord/lib/arel/delete_manager.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class DeleteManager < Arel::TreeManager
+ def initialize
+ super
+ @ast = Nodes::DeleteStatement.new
+ @ctx = @ast
+ end
+
+ def from(relation)
+ @ast.relation = relation
+ self
+ end
+
+ def take(limit)
+ @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit
+ self
+ end
+
+ def wheres=(list)
+ @ast.wheres = list
+ end
+ end
+end
diff --git a/activerecord/lib/arel/errors.rb b/activerecord/lib/arel/errors.rb
new file mode 100644
index 0000000000..2f8d5e3c02
--- /dev/null
+++ b/activerecord/lib/arel/errors.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class ArelError < StandardError
+ end
+
+ class EmptyJoinError < ArelError
+ end
+end
diff --git a/activerecord/lib/arel/expressions.rb b/activerecord/lib/arel/expressions.rb
new file mode 100644
index 0000000000..da8afb338c
--- /dev/null
+++ b/activerecord/lib/arel/expressions.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Expressions
+ def count(distinct = false)
+ Nodes::Count.new [self], distinct
+ end
+
+ def sum
+ Nodes::Sum.new [self]
+ end
+
+ def maximum
+ Nodes::Max.new [self]
+ end
+
+ def minimum
+ Nodes::Min.new [self]
+ end
+
+ def average
+ Nodes::Avg.new [self]
+ end
+
+ def extract(field)
+ Nodes::Extract.new [self], field
+ end
+ end
+end
diff --git a/activerecord/lib/arel/factory_methods.rb b/activerecord/lib/arel/factory_methods.rb
new file mode 100644
index 0000000000..b828bc274e
--- /dev/null
+++ b/activerecord/lib/arel/factory_methods.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ ###
+ # Methods for creating various nodes
+ module FactoryMethods
+ def create_true
+ Arel::Nodes::True.new
+ end
+
+ def create_false
+ Arel::Nodes::False.new
+ end
+
+ def create_table_alias(relation, name)
+ Nodes::TableAlias.new(relation, name)
+ end
+
+ def create_join(to, constraint = nil, klass = Nodes::InnerJoin)
+ klass.new(to, constraint)
+ end
+
+ def create_string_join(to)
+ create_join to, nil, Nodes::StringJoin
+ end
+
+ def create_and(clauses)
+ Nodes::And.new clauses
+ end
+
+ def create_on(expr)
+ Nodes::On.new expr
+ end
+
+ def grouping(expr)
+ Nodes::Grouping.new expr
+ end
+
+ ###
+ # Create a LOWER() function
+ def lower(column)
+ Nodes::NamedFunction.new "LOWER", [Nodes.build_quoted(column)]
+ end
+ end
+end
diff --git a/activerecord/lib/arel/insert_manager.rb b/activerecord/lib/arel/insert_manager.rb
new file mode 100644
index 0000000000..c90fc33a48
--- /dev/null
+++ b/activerecord/lib/arel/insert_manager.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class InsertManager < Arel::TreeManager
+ def initialize
+ super
+ @ast = Nodes::InsertStatement.new
+ end
+
+ def into(table)
+ @ast.relation = table
+ self
+ end
+
+ def columns; @ast.columns end
+ def values=(val); @ast.values = val; end
+
+ def select(select)
+ @ast.select = select
+ end
+
+ def insert(fields)
+ return if fields.empty?
+
+ if String === fields
+ @ast.values = Nodes::SqlLiteral.new(fields)
+ else
+ @ast.relation ||= fields.first.first.relation
+
+ values = []
+
+ fields.each do |column, value|
+ @ast.columns << column
+ values << value
+ end
+ @ast.values = create_values values, @ast.columns
+ end
+ self
+ end
+
+ def create_values(values, columns)
+ Nodes::Values.new values, columns
+ end
+
+ def create_values_list(rows)
+ Nodes::ValuesList.new(rows)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/math.rb b/activerecord/lib/arel/math.rb
new file mode 100644
index 0000000000..2359f13148
--- /dev/null
+++ b/activerecord/lib/arel/math.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Math
+ def *(other)
+ Arel::Nodes::Multiplication.new(self, other)
+ end
+
+ def +(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::Addition.new(self, other))
+ end
+
+ def -(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::Subtraction.new(self, other))
+ end
+
+ def /(other)
+ Arel::Nodes::Division.new(self, other)
+ end
+
+ def &(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseAnd.new(self, other))
+ end
+
+ def |(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(self, other))
+ end
+
+ def ^(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseXor.new(self, other))
+ end
+
+ def <<(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseShiftLeft.new(self, other))
+ end
+
+ def >>(other)
+ Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseShiftRight.new(self, other))
+ end
+
+ def ~@
+ Arel::Nodes::BitwiseNot.new(self)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes.rb b/activerecord/lib/arel/nodes.rb
new file mode 100644
index 0000000000..5af0e532e2
--- /dev/null
+++ b/activerecord/lib/arel/nodes.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+# node
+require "arel/nodes/node"
+require "arel/nodes/node_expression"
+require "arel/nodes/select_statement"
+require "arel/nodes/select_core"
+require "arel/nodes/insert_statement"
+require "arel/nodes/update_statement"
+require "arel/nodes/bind_param"
+
+# terminal
+
+require "arel/nodes/terminal"
+require "arel/nodes/true"
+require "arel/nodes/false"
+
+# unary
+require "arel/nodes/unary"
+require "arel/nodes/grouping"
+require "arel/nodes/ascending"
+require "arel/nodes/descending"
+require "arel/nodes/unqualified_column"
+require "arel/nodes/with"
+
+# binary
+require "arel/nodes/binary"
+require "arel/nodes/equality"
+require "arel/nodes/in" # Why is this subclassed from equality?
+require "arel/nodes/join_source"
+require "arel/nodes/delete_statement"
+require "arel/nodes/table_alias"
+require "arel/nodes/infix_operation"
+require "arel/nodes/unary_operation"
+require "arel/nodes/over"
+require "arel/nodes/matches"
+require "arel/nodes/regexp"
+
+# nary
+require "arel/nodes/and"
+
+# function
+# FIXME: Function + Alias can be rewritten as a Function and Alias node.
+# We should make Function a Unary node and deprecate the use of "aliaz"
+require "arel/nodes/function"
+require "arel/nodes/count"
+require "arel/nodes/extract"
+require "arel/nodes/values"
+require "arel/nodes/values_list"
+require "arel/nodes/named_function"
+
+# windows
+require "arel/nodes/window"
+
+# conditional expressions
+require "arel/nodes/case"
+
+# joins
+require "arel/nodes/full_outer_join"
+require "arel/nodes/inner_join"
+require "arel/nodes/outer_join"
+require "arel/nodes/right_outer_join"
+require "arel/nodes/string_join"
+
+require "arel/nodes/sql_literal"
+
+require "arel/nodes/casted"
diff --git a/activerecord/lib/arel/nodes/and.rb b/activerecord/lib/arel/nodes/and.rb
new file mode 100644
index 0000000000..c530a77bfb
--- /dev/null
+++ b/activerecord/lib/arel/nodes/and.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class And < Arel::Nodes::Node
+ attr_reader :children
+
+ def initialize(children)
+ super()
+ @children = children
+ end
+
+ def left
+ children.first
+ end
+
+ def right
+ children[1]
+ end
+
+ def hash
+ children.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.children == other.children
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/ascending.rb b/activerecord/lib/arel/nodes/ascending.rb
new file mode 100644
index 0000000000..8b617f4df5
--- /dev/null
+++ b/activerecord/lib/arel/nodes/ascending.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Ascending < Ordering
+ def reverse
+ Descending.new(expr)
+ end
+
+ def direction
+ :asc
+ end
+
+ def ascending?
+ true
+ end
+
+ def descending?
+ false
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/binary.rb b/activerecord/lib/arel/nodes/binary.rb
new file mode 100644
index 0000000000..e184e99c73
--- /dev/null
+++ b/activerecord/lib/arel/nodes/binary.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Binary < Arel::Nodes::NodeExpression
+ attr_accessor :left, :right
+
+ def initialize(left, right)
+ super()
+ @left = left
+ @right = right
+ end
+
+ def initialize_copy(other)
+ super
+ @left = @left.clone if @left
+ @right = @right.clone if @right
+ end
+
+ def hash
+ [self.class, @left, @right].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.left == other.left &&
+ self.right == other.right
+ end
+ alias :== :eql?
+ end
+
+ %w{
+ As
+ Assignment
+ Between
+ GreaterThan
+ GreaterThanOrEqual
+ Join
+ LessThan
+ LessThanOrEqual
+ NotEqual
+ NotIn
+ Or
+ Union
+ UnionAll
+ Intersect
+ Except
+ }.each do |name|
+ const_set name, Class.new(Binary)
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/bind_param.rb b/activerecord/lib/arel/nodes/bind_param.rb
new file mode 100644
index 0000000000..53c5563d93
--- /dev/null
+++ b/activerecord/lib/arel/nodes/bind_param.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class BindParam < Node
+ attr_accessor :value
+
+ def initialize(value)
+ @value = value
+ super()
+ end
+
+ def hash
+ [self.class, self.value].hash
+ end
+
+ def eql?(other)
+ other.is_a?(BindParam) &&
+ value == other.value
+ end
+ alias :== :eql?
+
+ def nil?
+ value.nil?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/case.rb b/activerecord/lib/arel/nodes/case.rb
new file mode 100644
index 0000000000..654a54825e
--- /dev/null
+++ b/activerecord/lib/arel/nodes/case.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Case < Arel::Nodes::Node
+ attr_accessor :case, :conditions, :default
+
+ def initialize(expression = nil, default = nil)
+ @case = expression
+ @conditions = []
+ @default = default
+ end
+
+ def when(condition, expression = nil)
+ @conditions << When.new(Nodes.build_quoted(condition), expression)
+ self
+ end
+
+ def then(expression)
+ @conditions.last.right = Nodes.build_quoted(expression)
+ self
+ end
+
+ def else(expression)
+ @default = Else.new Nodes.build_quoted(expression)
+ self
+ end
+
+ def initialize_copy(other)
+ super
+ @case = @case.clone if @case
+ @conditions = @conditions.map { |x| x.clone }
+ @default = @default.clone if @default
+ end
+
+ def hash
+ [@case, @conditions, @default].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.case == other.case &&
+ self.conditions == other.conditions &&
+ self.default == other.default
+ end
+ alias :== :eql?
+ end
+
+ class When < Binary # :nodoc:
+ end
+
+ class Else < Unary # :nodoc:
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/casted.rb b/activerecord/lib/arel/nodes/casted.rb
new file mode 100644
index 0000000000..c1e6e97d6d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/casted.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Casted < Arel::Nodes::NodeExpression # :nodoc:
+ attr_reader :val, :attribute
+ def initialize(val, attribute)
+ @val = val
+ @attribute = attribute
+ super()
+ end
+
+ def nil?; @val.nil?; end
+
+ def hash
+ [self.class, val, attribute].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.val == other.val &&
+ self.attribute == other.attribute
+ end
+ alias :== :eql?
+ end
+
+ class Quoted < Arel::Nodes::Unary # :nodoc:
+ alias :val :value
+ def nil?; val.nil?; end
+ end
+
+ def self.build_quoted(other, attribute = nil)
+ case other
+ when Arel::Nodes::Node, Arel::Attributes::Attribute, Arel::Table, Arel::Nodes::BindParam, Arel::SelectManager, Arel::Nodes::Quoted, Arel::Nodes::SqlLiteral
+ other
+ else
+ case attribute
+ when Arel::Attributes::Attribute
+ Casted.new other, attribute
+ else
+ Quoted.new other
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/count.rb b/activerecord/lib/arel/nodes/count.rb
new file mode 100644
index 0000000000..880464639d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/count.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Count < Arel::Nodes::Function
+ def initialize(expr, distinct = false, aliaz = nil)
+ super(expr, aliaz)
+ @distinct = distinct
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/delete_statement.rb b/activerecord/lib/arel/nodes/delete_statement.rb
new file mode 100644
index 0000000000..eaac05e2f6
--- /dev/null
+++ b/activerecord/lib/arel/nodes/delete_statement.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class DeleteStatement < Arel::Nodes::Node
+ attr_accessor :left, :right
+ attr_accessor :limit
+
+ alias :relation :left
+ alias :relation= :left=
+ alias :wheres :right
+ alias :wheres= :right=
+
+ def initialize(relation = nil, wheres = [])
+ super()
+ @left = relation
+ @right = wheres
+ end
+
+ def initialize_copy(other)
+ super
+ @left = @left.clone if @left
+ @right = @right.clone if @right
+ end
+
+ def hash
+ [self.class, @left, @right].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.left == other.left &&
+ self.right == other.right
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/descending.rb b/activerecord/lib/arel/nodes/descending.rb
new file mode 100644
index 0000000000..f3f6992ca8
--- /dev/null
+++ b/activerecord/lib/arel/nodes/descending.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Descending < Ordering
+ def reverse
+ Ascending.new(expr)
+ end
+
+ def direction
+ :desc
+ end
+
+ def ascending?
+ false
+ end
+
+ def descending?
+ true
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/equality.rb b/activerecord/lib/arel/nodes/equality.rb
new file mode 100644
index 0000000000..2aa85a977e
--- /dev/null
+++ b/activerecord/lib/arel/nodes/equality.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Equality < Arel::Nodes::Binary
+ def operator; :== end
+ alias :operand1 :left
+ alias :operand2 :right
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/extract.rb b/activerecord/lib/arel/nodes/extract.rb
new file mode 100644
index 0000000000..5799ee9b8f
--- /dev/null
+++ b/activerecord/lib/arel/nodes/extract.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Extract < Arel::Nodes::Unary
+ attr_accessor :field
+
+ def initialize(expr, field)
+ super(expr)
+ @field = field
+ end
+
+ def hash
+ super ^ @field.hash
+ end
+
+ def eql?(other)
+ super &&
+ self.field == other.field
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/false.rb b/activerecord/lib/arel/nodes/false.rb
new file mode 100644
index 0000000000..1e5bf04be5
--- /dev/null
+++ b/activerecord/lib/arel/nodes/false.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class False < Arel::Nodes::NodeExpression
+ def hash
+ self.class.hash
+ end
+
+ def eql?(other)
+ self.class == other.class
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/full_outer_join.rb b/activerecord/lib/arel/nodes/full_outer_join.rb
new file mode 100644
index 0000000000..91bb81f2e3
--- /dev/null
+++ b/activerecord/lib/arel/nodes/full_outer_join.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class FullOuterJoin < Arel::Nodes::Join
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/function.rb b/activerecord/lib/arel/nodes/function.rb
new file mode 100644
index 0000000000..0a439b39f5
--- /dev/null
+++ b/activerecord/lib/arel/nodes/function.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Function < Arel::Nodes::NodeExpression
+ include Arel::WindowPredications
+ attr_accessor :expressions, :alias, :distinct
+
+ def initialize(expr, aliaz = nil)
+ super()
+ @expressions = expr
+ @alias = aliaz && SqlLiteral.new(aliaz)
+ @distinct = false
+ end
+
+ def as(aliaz)
+ self.alias = SqlLiteral.new(aliaz)
+ self
+ end
+
+ def hash
+ [@expressions, @alias, @distinct].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.expressions == other.expressions &&
+ self.alias == other.alias &&
+ self.distinct == other.distinct
+ end
+ alias :== :eql?
+ end
+
+ %w{
+ Sum
+ Exists
+ Max
+ Min
+ Avg
+ }.each do |name|
+ const_set(name, Class.new(Function))
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/grouping.rb b/activerecord/lib/arel/nodes/grouping.rb
new file mode 100644
index 0000000000..4d0bd69d4d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/grouping.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Grouping < Unary
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/in.rb b/activerecord/lib/arel/nodes/in.rb
new file mode 100644
index 0000000000..2be45d6f99
--- /dev/null
+++ b/activerecord/lib/arel/nodes/in.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class In < Equality
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/infix_operation.rb b/activerecord/lib/arel/nodes/infix_operation.rb
new file mode 100644
index 0000000000..bc7e20dcc6
--- /dev/null
+++ b/activerecord/lib/arel/nodes/infix_operation.rb
@@ -0,0 +1,80 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class InfixOperation < Binary
+ include Arel::Expressions
+ include Arel::Predications
+ include Arel::OrderPredications
+ include Arel::AliasPredication
+ include Arel::Math
+
+ attr_reader :operator
+
+ def initialize(operator, left, right)
+ super(left, right)
+ @operator = operator
+ end
+ end
+
+ class Multiplication < InfixOperation
+ def initialize(left, right)
+ super(:*, left, right)
+ end
+ end
+
+ class Division < InfixOperation
+ def initialize(left, right)
+ super(:/, left, right)
+ end
+ end
+
+ class Addition < InfixOperation
+ def initialize(left, right)
+ super(:+, left, right)
+ end
+ end
+
+ class Subtraction < InfixOperation
+ def initialize(left, right)
+ super(:-, left, right)
+ end
+ end
+
+ class Concat < InfixOperation
+ def initialize(left, right)
+ super("||", left, right)
+ end
+ end
+
+ class BitwiseAnd < InfixOperation
+ def initialize(left, right)
+ super(:&, left, right)
+ end
+ end
+
+ class BitwiseOr < InfixOperation
+ def initialize(left, right)
+ super(:|, left, right)
+ end
+ end
+
+ class BitwiseXor < InfixOperation
+ def initialize(left, right)
+ super(:^, left, right)
+ end
+ end
+
+ class BitwiseShiftLeft < InfixOperation
+ def initialize(left, right)
+ super(:<<, left, right)
+ end
+ end
+
+ class BitwiseShiftRight < InfixOperation
+ def initialize(left, right)
+ super(:>>, left, right)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/inner_join.rb b/activerecord/lib/arel/nodes/inner_join.rb
new file mode 100644
index 0000000000..519fafad09
--- /dev/null
+++ b/activerecord/lib/arel/nodes/inner_join.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class InnerJoin < Arel::Nodes::Join
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/insert_statement.rb b/activerecord/lib/arel/nodes/insert_statement.rb
new file mode 100644
index 0000000000..d28fd1f6c8
--- /dev/null
+++ b/activerecord/lib/arel/nodes/insert_statement.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class InsertStatement < Arel::Nodes::Node
+ attr_accessor :relation, :columns, :values, :select
+
+ def initialize
+ super()
+ @relation = nil
+ @columns = []
+ @values = nil
+ @select = nil
+ end
+
+ def initialize_copy(other)
+ super
+ @columns = @columns.clone
+ @values = @values.clone if @values
+ @select = @select.clone if @select
+ end
+
+ def hash
+ [@relation, @columns, @values, @select].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.relation == other.relation &&
+ self.columns == other.columns &&
+ self.select == other.select &&
+ self.values == other.values
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/join_source.rb b/activerecord/lib/arel/nodes/join_source.rb
new file mode 100644
index 0000000000..abf0944623
--- /dev/null
+++ b/activerecord/lib/arel/nodes/join_source.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ ###
+ # Class that represents a join source
+ #
+ # http://www.sqlite.org/syntaxdiagrams.html#join-source
+
+ class JoinSource < Arel::Nodes::Binary
+ def initialize(single_source, joinop = [])
+ super
+ end
+
+ def empty?
+ !left && right.empty?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/matches.rb b/activerecord/lib/arel/nodes/matches.rb
new file mode 100644
index 0000000000..fd5734f4bd
--- /dev/null
+++ b/activerecord/lib/arel/nodes/matches.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Matches < Binary
+ attr_reader :escape
+ attr_accessor :case_sensitive
+
+ def initialize(left, right, escape = nil, case_sensitive = false)
+ super(left, right)
+ @escape = escape && Nodes.build_quoted(escape)
+ @case_sensitive = case_sensitive
+ end
+ end
+
+ class DoesNotMatch < Matches; end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/named_function.rb b/activerecord/lib/arel/nodes/named_function.rb
new file mode 100644
index 0000000000..126462d6d6
--- /dev/null
+++ b/activerecord/lib/arel/nodes/named_function.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class NamedFunction < Arel::Nodes::Function
+ attr_accessor :name
+
+ def initialize(name, expr, aliaz = nil)
+ super(expr, aliaz)
+ @name = name
+ end
+
+ def hash
+ super ^ @name.hash
+ end
+
+ def eql?(other)
+ super && self.name == other.name
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/node.rb b/activerecord/lib/arel/nodes/node.rb
new file mode 100644
index 0000000000..2b9b1e9828
--- /dev/null
+++ b/activerecord/lib/arel/nodes/node.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ ###
+ # Abstract base class for all AST nodes
+ class Node
+ include Arel::FactoryMethods
+ include Enumerable
+
+ if $DEBUG
+ def _caller
+ @caller
+ end
+
+ def initialize
+ @caller = caller.dup
+ end
+ end
+
+ ###
+ # Factory method to create a Nodes::Not node that has the recipient of
+ # the caller as a child.
+ def not
+ Nodes::Not.new self
+ end
+
+ ###
+ # Factory method to create a Nodes::Grouping node that has an Nodes::Or
+ # node as a child.
+ def or(right)
+ Nodes::Grouping.new Nodes::Or.new(self, right)
+ end
+
+ ###
+ # Factory method to create an Nodes::And node.
+ def and(right)
+ Nodes::And.new [self, right]
+ end
+
+ # FIXME: this method should go away. I don't like people calling
+ # to_sql on non-head nodes. This forces us to walk the AST until we
+ # can find a node that has a "relation" member.
+ #
+ # Maybe we should just use `Table.engine`? :'(
+ def to_sql(engine = Table.engine)
+ collector = Arel::Collectors::SQLString.new
+ collector = engine.connection.visitor.accept self, collector
+ collector.value
+ end
+
+ # Iterate through AST, nodes will be yielded depth-first
+ def each(&block)
+ return enum_for(:each) unless block_given?
+
+ ::Arel::Visitors::DepthFirst.new(block).accept self
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/node_expression.rb b/activerecord/lib/arel/nodes/node_expression.rb
new file mode 100644
index 0000000000..cbcfaba37c
--- /dev/null
+++ b/activerecord/lib/arel/nodes/node_expression.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class NodeExpression < Arel::Nodes::Node
+ include Arel::Expressions
+ include Arel::Predications
+ include Arel::AliasPredication
+ include Arel::OrderPredications
+ include Arel::Math
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/outer_join.rb b/activerecord/lib/arel/nodes/outer_join.rb
new file mode 100644
index 0000000000..0a3042be61
--- /dev/null
+++ b/activerecord/lib/arel/nodes/outer_join.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class OuterJoin < Arel::Nodes::Join
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/over.rb b/activerecord/lib/arel/nodes/over.rb
new file mode 100644
index 0000000000..91176764a9
--- /dev/null
+++ b/activerecord/lib/arel/nodes/over.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Over < Binary
+ include Arel::AliasPredication
+
+ def initialize(left, right = nil)
+ super(left, right)
+ end
+
+ def operator; "OVER" end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/regexp.rb b/activerecord/lib/arel/nodes/regexp.rb
new file mode 100644
index 0000000000..7c25095569
--- /dev/null
+++ b/activerecord/lib/arel/nodes/regexp.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Regexp < Binary
+ attr_accessor :case_sensitive
+
+ def initialize(left, right, case_sensitive = true)
+ super(left, right)
+ @case_sensitive = case_sensitive
+ end
+ end
+
+ class NotRegexp < Regexp; end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/right_outer_join.rb b/activerecord/lib/arel/nodes/right_outer_join.rb
new file mode 100644
index 0000000000..04ed4aaa78
--- /dev/null
+++ b/activerecord/lib/arel/nodes/right_outer_join.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class RightOuterJoin < Arel::Nodes::Join
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/select_core.rb b/activerecord/lib/arel/nodes/select_core.rb
new file mode 100644
index 0000000000..2defe61974
--- /dev/null
+++ b/activerecord/lib/arel/nodes/select_core.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class SelectCore < Arel::Nodes::Node
+ attr_accessor :top, :projections, :wheres, :groups, :windows
+ attr_accessor :havings, :source, :set_quantifier
+
+ def initialize
+ super()
+ @source = JoinSource.new nil
+ @top = nil
+
+ # https://ronsavage.github.io/SQL/sql-92.bnf.html#set%20quantifier
+ @set_quantifier = nil
+ @projections = []
+ @wheres = []
+ @groups = []
+ @havings = []
+ @windows = []
+ end
+
+ def from
+ @source.left
+ end
+
+ def from=(value)
+ @source.left = value
+ end
+
+ alias :froms= :from=
+ alias :froms :from
+
+ def initialize_copy(other)
+ super
+ @source = @source.clone if @source
+ @projections = @projections.clone
+ @wheres = @wheres.clone
+ @groups = @groups.clone
+ @havings = @havings.clone
+ @windows = @windows.clone
+ end
+
+ def hash
+ [
+ @source, @top, @set_quantifier, @projections,
+ @wheres, @groups, @havings, @windows
+ ].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.source == other.source &&
+ self.top == other.top &&
+ self.set_quantifier == other.set_quantifier &&
+ self.projections == other.projections &&
+ self.wheres == other.wheres &&
+ self.groups == other.groups &&
+ self.havings == other.havings &&
+ self.windows == other.windows
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/select_statement.rb b/activerecord/lib/arel/nodes/select_statement.rb
new file mode 100644
index 0000000000..eff5dad939
--- /dev/null
+++ b/activerecord/lib/arel/nodes/select_statement.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class SelectStatement < Arel::Nodes::NodeExpression
+ attr_reader :cores
+ attr_accessor :limit, :orders, :lock, :offset, :with
+
+ def initialize(cores = [SelectCore.new])
+ super()
+ @cores = cores
+ @orders = []
+ @limit = nil
+ @lock = nil
+ @offset = nil
+ @with = nil
+ end
+
+ def initialize_copy(other)
+ super
+ @cores = @cores.map { |x| x.clone }
+ @orders = @orders.map { |x| x.clone }
+ end
+
+ def hash
+ [@cores, @orders, @limit, @lock, @offset, @with].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.cores == other.cores &&
+ self.orders == other.orders &&
+ self.limit == other.limit &&
+ self.lock == other.lock &&
+ self.offset == other.offset &&
+ self.with == other.with
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/sql_literal.rb b/activerecord/lib/arel/nodes/sql_literal.rb
new file mode 100644
index 0000000000..d25a8521b7
--- /dev/null
+++ b/activerecord/lib/arel/nodes/sql_literal.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class SqlLiteral < String
+ include Arel::Expressions
+ include Arel::Predications
+ include Arel::AliasPredication
+ include Arel::OrderPredications
+
+ def encode_with(coder)
+ coder.scalar = self.to_s
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/string_join.rb b/activerecord/lib/arel/nodes/string_join.rb
new file mode 100644
index 0000000000..86027fcab7
--- /dev/null
+++ b/activerecord/lib/arel/nodes/string_join.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class StringJoin < Arel::Nodes::Join
+ def initialize(left, right = nil)
+ super
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/table_alias.rb b/activerecord/lib/arel/nodes/table_alias.rb
new file mode 100644
index 0000000000..f95ca16a3d
--- /dev/null
+++ b/activerecord/lib/arel/nodes/table_alias.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class TableAlias < Arel::Nodes::Binary
+ alias :name :right
+ alias :relation :left
+ alias :table_alias :name
+
+ def [](name)
+ Attribute.new(self, name)
+ end
+
+ def table_name
+ relation.respond_to?(:name) ? relation.name : name
+ end
+
+ def type_cast_for_database(*args)
+ relation.type_cast_for_database(*args)
+ end
+
+ def able_to_type_cast?
+ relation.respond_to?(:able_to_type_cast?) && relation.able_to_type_cast?
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/terminal.rb b/activerecord/lib/arel/nodes/terminal.rb
new file mode 100644
index 0000000000..d84c453f1a
--- /dev/null
+++ b/activerecord/lib/arel/nodes/terminal.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Distinct < Arel::Nodes::NodeExpression
+ def hash
+ self.class.hash
+ end
+
+ def eql?(other)
+ self.class == other.class
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/true.rb b/activerecord/lib/arel/nodes/true.rb
new file mode 100644
index 0000000000..c891012969
--- /dev/null
+++ b/activerecord/lib/arel/nodes/true.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class True < Arel::Nodes::NodeExpression
+ def hash
+ self.class.hash
+ end
+
+ def eql?(other)
+ self.class == other.class
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/unary.rb b/activerecord/lib/arel/nodes/unary.rb
new file mode 100644
index 0000000000..a3c0045897
--- /dev/null
+++ b/activerecord/lib/arel/nodes/unary.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Unary < Arel::Nodes::NodeExpression
+ attr_accessor :expr
+ alias :value :expr
+
+ def initialize(expr)
+ super()
+ @expr = expr
+ end
+
+ def hash
+ @expr.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.expr == other.expr
+ end
+ alias :== :eql?
+ end
+
+ %w{
+ Bin
+ Cube
+ DistinctOn
+ Group
+ GroupingElement
+ GroupingSet
+ Lateral
+ Limit
+ Lock
+ Not
+ Offset
+ On
+ Ordering
+ RollUp
+ Top
+ }.each do |name|
+ const_set(name, Class.new(Unary))
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/unary_operation.rb b/activerecord/lib/arel/nodes/unary_operation.rb
new file mode 100644
index 0000000000..524282ac84
--- /dev/null
+++ b/activerecord/lib/arel/nodes/unary_operation.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class UnaryOperation < Unary
+ attr_reader :operator
+
+ def initialize(operator, operand)
+ super(operand)
+ @operator = operator
+ end
+ end
+
+ class BitwiseNot < UnaryOperation
+ def initialize(operand)
+ super(:~, operand)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/unqualified_column.rb b/activerecord/lib/arel/nodes/unqualified_column.rb
new file mode 100644
index 0000000000..7c3e0720d7
--- /dev/null
+++ b/activerecord/lib/arel/nodes/unqualified_column.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class UnqualifiedColumn < Arel::Nodes::Unary
+ alias :attribute :expr
+ alias :attribute= :expr=
+
+ def relation
+ @expr.relation
+ end
+
+ def column
+ @expr.column
+ end
+
+ def name
+ @expr.name
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/update_statement.rb b/activerecord/lib/arel/nodes/update_statement.rb
new file mode 100644
index 0000000000..5184b1180f
--- /dev/null
+++ b/activerecord/lib/arel/nodes/update_statement.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class UpdateStatement < Arel::Nodes::Node
+ attr_accessor :relation, :wheres, :values, :orders, :limit
+ attr_accessor :key
+
+ def initialize
+ @relation = nil
+ @wheres = []
+ @values = []
+ @orders = []
+ @limit = nil
+ @key = nil
+ end
+
+ def initialize_copy(other)
+ super
+ @wheres = @wheres.clone
+ @values = @values.clone
+ end
+
+ def hash
+ [@relation, @wheres, @values, @orders, @limit, @key].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.relation == other.relation &&
+ self.wheres == other.wheres &&
+ self.values == other.values &&
+ self.orders == other.orders &&
+ self.limit == other.limit &&
+ self.key == other.key
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/values.rb b/activerecord/lib/arel/nodes/values.rb
new file mode 100644
index 0000000000..650248dc04
--- /dev/null
+++ b/activerecord/lib/arel/nodes/values.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Values < Arel::Nodes::Binary
+ alias :expressions :left
+ alias :expressions= :left=
+ alias :columns :right
+ alias :columns= :right=
+
+ def initialize(exprs, columns = [])
+ super
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/values_list.rb b/activerecord/lib/arel/nodes/values_list.rb
new file mode 100644
index 0000000000..27109848e4
--- /dev/null
+++ b/activerecord/lib/arel/nodes/values_list.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class ValuesList < Node
+ attr_reader :rows
+
+ def initialize(rows)
+ @rows = rows
+ super()
+ end
+
+ def hash
+ @rows.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.rows == other.rows
+ end
+ alias :== :eql?
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/window.rb b/activerecord/lib/arel/nodes/window.rb
new file mode 100644
index 0000000000..4916fc7fbe
--- /dev/null
+++ b/activerecord/lib/arel/nodes/window.rb
@@ -0,0 +1,126 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class Window < Arel::Nodes::Node
+ attr_accessor :orders, :framing, :partitions
+
+ def initialize
+ @orders = []
+ @partitions = []
+ @framing = nil
+ end
+
+ def order(*expr)
+ # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically
+ @orders.concat expr.map { |x|
+ String === x || Symbol === x ? Nodes::SqlLiteral.new(x.to_s) : x
+ }
+ self
+ end
+
+ def partition(*expr)
+ # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically
+ @partitions.concat expr.map { |x|
+ String === x || Symbol === x ? Nodes::SqlLiteral.new(x.to_s) : x
+ }
+ self
+ end
+
+ def frame(expr)
+ @framing = expr
+ end
+
+ def rows(expr = nil)
+ if @framing
+ Rows.new(expr)
+ else
+ frame(Rows.new(expr))
+ end
+ end
+
+ def range(expr = nil)
+ if @framing
+ Range.new(expr)
+ else
+ frame(Range.new(expr))
+ end
+ end
+
+ def initialize_copy(other)
+ super
+ @orders = @orders.map { |x| x.clone }
+ end
+
+ def hash
+ [@orders, @framing].hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.orders == other.orders &&
+ self.framing == other.framing &&
+ self.partitions == other.partitions
+ end
+ alias :== :eql?
+ end
+
+ class NamedWindow < Window
+ attr_accessor :name
+
+ def initialize(name)
+ super()
+ @name = name
+ end
+
+ def initialize_copy(other)
+ super
+ @name = other.name.clone
+ end
+
+ def hash
+ super ^ @name.hash
+ end
+
+ def eql?(other)
+ super && self.name == other.name
+ end
+ alias :== :eql?
+ end
+
+ class Rows < Unary
+ def initialize(expr = nil)
+ super(expr)
+ end
+ end
+
+ class Range < Unary
+ def initialize(expr = nil)
+ super(expr)
+ end
+ end
+
+ class CurrentRow < Node
+ def hash
+ self.class.hash
+ end
+
+ def eql?(other)
+ self.class == other.class
+ end
+ alias :== :eql?
+ end
+
+ class Preceding < Unary
+ def initialize(expr = nil)
+ super(expr)
+ end
+ end
+
+ class Following < Unary
+ def initialize(expr = nil)
+ super(expr)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/nodes/with.rb b/activerecord/lib/arel/nodes/with.rb
new file mode 100644
index 0000000000..157bdcaa08
--- /dev/null
+++ b/activerecord/lib/arel/nodes/with.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Nodes
+ class With < Arel::Nodes::Unary
+ alias children expr
+ end
+
+ class WithRecursive < With; end
+ end
+end
diff --git a/activerecord/lib/arel/order_predications.rb b/activerecord/lib/arel/order_predications.rb
new file mode 100644
index 0000000000..d785bbba92
--- /dev/null
+++ b/activerecord/lib/arel/order_predications.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module OrderPredications
+ def asc
+ Nodes::Ascending.new self
+ end
+
+ def desc
+ Nodes::Descending.new self
+ end
+ end
+end
diff --git a/activerecord/lib/arel/predications.rb b/activerecord/lib/arel/predications.rb
new file mode 100644
index 0000000000..e83a6f162f
--- /dev/null
+++ b/activerecord/lib/arel/predications.rb
@@ -0,0 +1,241 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Predications
+ def not_eq(other)
+ Nodes::NotEqual.new self, quoted_node(other)
+ end
+
+ def not_eq_any(others)
+ grouping_any :not_eq, others
+ end
+
+ def not_eq_all(others)
+ grouping_all :not_eq, others
+ end
+
+ def eq(other)
+ Nodes::Equality.new self, quoted_node(other)
+ end
+
+ def eq_any(others)
+ grouping_any :eq, others
+ end
+
+ def eq_all(others)
+ grouping_all :eq, quoted_array(others)
+ end
+
+ def between(other)
+ if equals_quoted?(other.begin, -Float::INFINITY)
+ if equals_quoted?(other.end, Float::INFINITY)
+ not_in([])
+ elsif other.exclude_end?
+ lt(other.end)
+ else
+ lteq(other.end)
+ end
+ elsif equals_quoted?(other.end, Float::INFINITY)
+ gteq(other.begin)
+ elsif other.exclude_end?
+ gteq(other.begin).and(lt(other.end))
+ else
+ left = quoted_node(other.begin)
+ right = quoted_node(other.end)
+ Nodes::Between.new(self, left.and(right))
+ end
+ end
+
+ def in(other)
+ case other
+ when Arel::SelectManager
+ Arel::Nodes::In.new(self, other.ast)
+ when Range
+ if $VERBOSE
+ warn <<-eowarn
+Passing a range to `#in` is deprecated. Call `#between`, instead.
+ eowarn
+ end
+ between(other)
+ when Enumerable
+ Nodes::In.new self, quoted_array(other)
+ else
+ Nodes::In.new self, quoted_node(other)
+ end
+ end
+
+ def in_any(others)
+ grouping_any :in, others
+ end
+
+ def in_all(others)
+ grouping_all :in, others
+ end
+
+ def not_between(other)
+ if equals_quoted?(other.begin, -Float::INFINITY)
+ if equals_quoted?(other.end, Float::INFINITY)
+ self.in([])
+ elsif other.exclude_end?
+ gteq(other.end)
+ else
+ gt(other.end)
+ end
+ elsif equals_quoted?(other.end, Float::INFINITY)
+ lt(other.begin)
+ else
+ left = lt(other.begin)
+ right = if other.exclude_end?
+ gteq(other.end)
+ else
+ gt(other.end)
+ end
+ left.or(right)
+ end
+ end
+
+ def not_in(other)
+ case other
+ when Arel::SelectManager
+ Arel::Nodes::NotIn.new(self, other.ast)
+ when Range
+ if $VERBOSE
+ warn <<-eowarn
+Passing a range to `#not_in` is deprecated. Call `#not_between`, instead.
+ eowarn
+ end
+ not_between(other)
+ when Enumerable
+ Nodes::NotIn.new self, quoted_array(other)
+ else
+ Nodes::NotIn.new self, quoted_node(other)
+ end
+ end
+
+ def not_in_any(others)
+ grouping_any :not_in, others
+ end
+
+ def not_in_all(others)
+ grouping_all :not_in, others
+ end
+
+ def matches(other, escape = nil, case_sensitive = false)
+ Nodes::Matches.new self, quoted_node(other), escape, case_sensitive
+ end
+
+ def matches_regexp(other, case_sensitive = true)
+ Nodes::Regexp.new self, quoted_node(other), case_sensitive
+ end
+
+ def matches_any(others, escape = nil, case_sensitive = false)
+ grouping_any :matches, others, escape, case_sensitive
+ end
+
+ def matches_all(others, escape = nil, case_sensitive = false)
+ grouping_all :matches, others, escape, case_sensitive
+ end
+
+ def does_not_match(other, escape = nil, case_sensitive = false)
+ Nodes::DoesNotMatch.new self, quoted_node(other), escape, case_sensitive
+ end
+
+ def does_not_match_regexp(other, case_sensitive = true)
+ Nodes::NotRegexp.new self, quoted_node(other), case_sensitive
+ end
+
+ def does_not_match_any(others, escape = nil)
+ grouping_any :does_not_match, others, escape
+ end
+
+ def does_not_match_all(others, escape = nil)
+ grouping_all :does_not_match, others, escape
+ end
+
+ def gteq(right)
+ Nodes::GreaterThanOrEqual.new self, quoted_node(right)
+ end
+
+ def gteq_any(others)
+ grouping_any :gteq, others
+ end
+
+ def gteq_all(others)
+ grouping_all :gteq, others
+ end
+
+ def gt(right)
+ Nodes::GreaterThan.new self, quoted_node(right)
+ end
+
+ def gt_any(others)
+ grouping_any :gt, others
+ end
+
+ def gt_all(others)
+ grouping_all :gt, others
+ end
+
+ def lt(right)
+ Nodes::LessThan.new self, quoted_node(right)
+ end
+
+ def lt_any(others)
+ grouping_any :lt, others
+ end
+
+ def lt_all(others)
+ grouping_all :lt, others
+ end
+
+ def lteq(right)
+ Nodes::LessThanOrEqual.new self, quoted_node(right)
+ end
+
+ def lteq_any(others)
+ grouping_any :lteq, others
+ end
+
+ def lteq_all(others)
+ grouping_all :lteq, others
+ end
+
+ def when(right)
+ Nodes::Case.new(self).when quoted_node(right)
+ end
+
+ def concat(other)
+ Nodes::Concat.new self, other
+ end
+
+ private
+
+ def grouping_any(method_id, others, *extras)
+ nodes = others.map { |expr| send(method_id, expr, *extras) }
+ Nodes::Grouping.new nodes.inject { |memo, node|
+ Nodes::Or.new(memo, node)
+ }
+ end
+
+ def grouping_all(method_id, others, *extras)
+ nodes = others.map { |expr| send(method_id, expr, *extras) }
+ Nodes::Grouping.new Nodes::And.new(nodes)
+ end
+
+ def quoted_node(other)
+ Nodes.build_quoted(other, self)
+ end
+
+ def quoted_array(others)
+ others.map { |v| quoted_node(v) }
+ end
+
+ def equals_quoted?(maybe_quoted, value)
+ if maybe_quoted.is_a?(Nodes::Quoted)
+ maybe_quoted.val == value
+ else
+ maybe_quoted == value
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/select_manager.rb b/activerecord/lib/arel/select_manager.rb
new file mode 100644
index 0000000000..22a04b00c6
--- /dev/null
+++ b/activerecord/lib/arel/select_manager.rb
@@ -0,0 +1,273 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class SelectManager < Arel::TreeManager
+ include Arel::Crud
+
+ STRING_OR_SYMBOL_CLASS = [Symbol, String]
+
+ def initialize(table = nil)
+ super()
+ @ast = Nodes::SelectStatement.new
+ @ctx = @ast.cores.last
+ from table
+ end
+
+ def initialize_copy(other)
+ super
+ @ctx = @ast.cores.last
+ end
+
+ def limit
+ @ast.limit && @ast.limit.expr
+ end
+ alias :taken :limit
+
+ def constraints
+ @ctx.wheres
+ end
+
+ def offset
+ @ast.offset && @ast.offset.expr
+ end
+
+ def skip(amount)
+ if amount
+ @ast.offset = Nodes::Offset.new(amount)
+ else
+ @ast.offset = nil
+ end
+ self
+ end
+ alias :offset= :skip
+
+ ###
+ # Produces an Arel::Nodes::Exists node
+ def exists
+ Arel::Nodes::Exists.new @ast
+ end
+
+ def as(other)
+ create_table_alias grouping(@ast), Nodes::SqlLiteral.new(other)
+ end
+
+ def lock(locking = Arel.sql("FOR UPDATE"))
+ case locking
+ when true
+ locking = Arel.sql("FOR UPDATE")
+ when Arel::Nodes::SqlLiteral
+ when String
+ locking = Arel.sql locking
+ end
+
+ @ast.lock = Nodes::Lock.new(locking)
+ self
+ end
+
+ def locked
+ @ast.lock
+ end
+
+ def on(*exprs)
+ @ctx.source.right.last.right = Nodes::On.new(collapse(exprs))
+ self
+ end
+
+ def group(*columns)
+ columns.each do |column|
+ # FIXME: backwards compat
+ column = Nodes::SqlLiteral.new(column) if String === column
+ column = Nodes::SqlLiteral.new(column.to_s) if Symbol === column
+
+ @ctx.groups.push Nodes::Group.new column
+ end
+ self
+ end
+
+ def from(table)
+ table = Nodes::SqlLiteral.new(table) if String === table
+
+ case table
+ when Nodes::Join
+ @ctx.source.right << table
+ else
+ @ctx.source.left = table
+ end
+
+ self
+ end
+
+ def froms
+ @ast.cores.map { |x| x.from }.compact
+ end
+
+ def join(relation, klass = Nodes::InnerJoin)
+ return self unless relation
+
+ case relation
+ when String, Nodes::SqlLiteral
+ raise EmptyJoinError if relation.empty?
+ klass = Nodes::StringJoin
+ end
+
+ @ctx.source.right << create_join(relation, nil, klass)
+ self
+ end
+
+ def outer_join(relation)
+ join(relation, Nodes::OuterJoin)
+ end
+
+ def having(expr)
+ @ctx.havings << expr
+ self
+ end
+
+ def window(name)
+ window = Nodes::NamedWindow.new(name)
+ @ctx.windows.push window
+ window
+ end
+
+ def project(*projections)
+ # FIXME: converting these to SQLLiterals is probably not good, but
+ # rails tests require it.
+ @ctx.projections.concat projections.map { |x|
+ STRING_OR_SYMBOL_CLASS.include?(x.class) ? Nodes::SqlLiteral.new(x.to_s) : x
+ }
+ self
+ end
+
+ def projections
+ @ctx.projections
+ end
+
+ def projections=(projections)
+ @ctx.projections = projections
+ end
+
+ def distinct(value = true)
+ if value
+ @ctx.set_quantifier = Arel::Nodes::Distinct.new
+ else
+ @ctx.set_quantifier = nil
+ end
+ self
+ end
+
+ def distinct_on(value)
+ if value
+ @ctx.set_quantifier = Arel::Nodes::DistinctOn.new(value)
+ else
+ @ctx.set_quantifier = nil
+ end
+ self
+ end
+
+ def order(*expr)
+ # FIXME: We SHOULD NOT be converting these to SqlLiteral automatically
+ @ast.orders.concat expr.map { |x|
+ STRING_OR_SYMBOL_CLASS.include?(x.class) ? Nodes::SqlLiteral.new(x.to_s) : x
+ }
+ self
+ end
+
+ def orders
+ @ast.orders
+ end
+
+ def where_sql(engine = Table.engine)
+ return if @ctx.wheres.empty?
+
+ viz = Visitors::WhereSql.new(engine.connection.visitor, engine.connection)
+ Nodes::SqlLiteral.new viz.accept(@ctx, Collectors::SQLString.new).value
+ end
+
+ def union(operation, other = nil)
+ if other
+ node_class = Nodes.const_get("Union#{operation.to_s.capitalize}")
+ else
+ other = operation
+ node_class = Nodes::Union
+ end
+
+ node_class.new self.ast, other.ast
+ end
+
+ def intersect(other)
+ Nodes::Intersect.new ast, other.ast
+ end
+
+ def except(other)
+ Nodes::Except.new ast, other.ast
+ end
+ alias :minus :except
+
+ def lateral(table_name = nil)
+ base = table_name.nil? ? ast : as(table_name)
+ Nodes::Lateral.new(base)
+ end
+
+ def with(*subqueries)
+ if subqueries.first.is_a? Symbol
+ node_class = Nodes.const_get("With#{subqueries.shift.to_s.capitalize}")
+ else
+ node_class = Nodes::With
+ end
+ @ast.with = node_class.new(subqueries.flatten)
+
+ self
+ end
+
+ def take(limit)
+ if limit
+ @ast.limit = Nodes::Limit.new(limit)
+ @ctx.top = Nodes::Top.new(limit)
+ else
+ @ast.limit = nil
+ @ctx.top = nil
+ end
+ self
+ end
+ alias limit= take
+
+ def join_sources
+ @ctx.source.right
+ end
+
+ def source
+ @ctx.source
+ end
+
+ class Row < Struct.new(:data) # :nodoc:
+ def id
+ data["id"]
+ end
+
+ def method_missing(name, *args)
+ name = name.to_s
+ return data[name] if data.key?(name)
+ super
+ end
+ end
+
+ private
+ def collapse(exprs, existing = nil)
+ exprs = exprs.unshift(existing.expr) if existing
+ exprs = exprs.compact.map { |expr|
+ if String === expr
+ # FIXME: Don't do this automatically
+ Arel.sql(expr)
+ else
+ expr
+ end
+ }
+
+ if exprs.length == 1
+ exprs.first
+ else
+ create_and exprs
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/table.rb b/activerecord/lib/arel/table.rb
new file mode 100644
index 0000000000..686fcdf962
--- /dev/null
+++ b/activerecord/lib/arel/table.rb
@@ -0,0 +1,111 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class Table
+ include Arel::Crud
+ include Arel::FactoryMethods
+
+ @engine = nil
+ class << self; attr_accessor :engine; end
+
+ attr_accessor :name, :table_alias
+
+ # TableAlias and Table both have a #table_name which is the name of the underlying table
+ alias :table_name :name
+
+ def initialize(name, as: nil, type_caster: nil)
+ @name = name.to_s
+ @type_caster = type_caster
+
+ # Sometime AR sends an :as parameter to table, to let the table know
+ # that it is an Alias. We may want to override new, and return a
+ # TableAlias node?
+ if as.to_s == @name
+ as = nil
+ end
+ @table_alias = as
+ end
+
+ def alias(name = "#{self.name}_2")
+ Nodes::TableAlias.new(self, name)
+ end
+
+ def from
+ SelectManager.new(self)
+ end
+
+ def join(relation, klass = Nodes::InnerJoin)
+ return from unless relation
+
+ case relation
+ when String, Nodes::SqlLiteral
+ raise EmptyJoinError if relation.empty?
+ klass = Nodes::StringJoin
+ end
+
+ from.join(relation, klass)
+ end
+
+ def outer_join(relation)
+ join(relation, Nodes::OuterJoin)
+ end
+
+ def group(*columns)
+ from.group(*columns)
+ end
+
+ def order(*expr)
+ from.order(*expr)
+ end
+
+ def where(condition)
+ from.where condition
+ end
+
+ def project(*things)
+ from.project(*things)
+ end
+
+ def take(amount)
+ from.take amount
+ end
+
+ def skip(amount)
+ from.skip amount
+ end
+
+ def having(expr)
+ from.having expr
+ end
+
+ def [](name)
+ ::Arel::Attribute.new self, name
+ end
+
+ def hash
+ # Perf note: aliases and table alias is excluded from the hash
+ # aliases can have a loop back to this table breaking hashes in parent
+ # relations, for the vast majority of cases @name is unique to a query
+ @name.hash
+ end
+
+ def eql?(other)
+ self.class == other.class &&
+ self.name == other.name &&
+ self.table_alias == other.table_alias
+ end
+ alias :== :eql?
+
+ def type_cast_for_database(attribute_name, value)
+ type_caster.type_cast_for_database(attribute_name, value)
+ end
+
+ def able_to_type_cast?
+ !type_caster.nil?
+ end
+
+ protected
+
+ attr_reader :type_caster
+ end
+end
diff --git a/activerecord/lib/arel/tree_manager.rb b/activerecord/lib/arel/tree_manager.rb
new file mode 100644
index 0000000000..ed47b09a37
--- /dev/null
+++ b/activerecord/lib/arel/tree_manager.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class TreeManager
+ include Arel::FactoryMethods
+
+ attr_reader :ast
+
+ def initialize
+ @ctx = nil
+ end
+
+ def to_dot
+ collector = Arel::Collectors::PlainString.new
+ collector = Visitors::Dot.new.accept @ast, collector
+ collector.value
+ end
+
+ def to_sql(engine = Table.engine)
+ collector = Arel::Collectors::SQLString.new
+ collector = engine.connection.visitor.accept @ast, collector
+ collector.value
+ end
+
+ def initialize_copy(other)
+ super
+ @ast = @ast.clone
+ end
+
+ def where(expr)
+ if Arel::TreeManager === expr
+ expr = expr.ast
+ end
+ @ctx.wheres << expr
+ self
+ end
+ end
+end
diff --git a/activerecord/lib/arel/update_manager.rb b/activerecord/lib/arel/update_manager.rb
new file mode 100644
index 0000000000..fe444343ba
--- /dev/null
+++ b/activerecord/lib/arel/update_manager.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ class UpdateManager < Arel::TreeManager
+ def initialize
+ super
+ @ast = Nodes::UpdateStatement.new
+ @ctx = @ast
+ end
+
+ def take(limit)
+ @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit
+ self
+ end
+
+ def key=(key)
+ @ast.key = Nodes.build_quoted(key)
+ end
+
+ def key
+ @ast.key
+ end
+
+ def order(*expr)
+ @ast.orders = expr
+ self
+ end
+
+ ###
+ # UPDATE +table+
+ def table(table)
+ @ast.relation = table
+ self
+ end
+
+ def wheres=(exprs)
+ @ast.wheres = exprs
+ end
+
+ def where(expr)
+ @ast.wheres << expr
+ self
+ end
+
+ def set(values)
+ if String === values
+ @ast.values = [values]
+ else
+ @ast.values = values.map { |column, value|
+ Nodes::Assignment.new(
+ Nodes::UnqualifiedColumn.new(column),
+ value
+ )
+ }
+ end
+ self
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors.rb b/activerecord/lib/arel/visitors.rb
new file mode 100644
index 0000000000..e350f52e65
--- /dev/null
+++ b/activerecord/lib/arel/visitors.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require "arel/visitors/visitor"
+require "arel/visitors/depth_first"
+require "arel/visitors/to_sql"
+require "arel/visitors/sqlite"
+require "arel/visitors/postgresql"
+require "arel/visitors/mysql"
+require "arel/visitors/mssql"
+require "arel/visitors/oracle"
+require "arel/visitors/oracle12"
+require "arel/visitors/where_sql"
+require "arel/visitors/dot"
+require "arel/visitors/ibm_db"
+require "arel/visitors/informix"
+
+module Arel # :nodoc: all
+ module Visitors
+ end
+end
diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb
new file mode 100644
index 0000000000..bcf8f8f980
--- /dev/null
+++ b/activerecord/lib/arel/visitors/depth_first.rb
@@ -0,0 +1,200 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class DepthFirst < Arel::Visitors::Visitor
+ def initialize(block = nil)
+ @block = block || Proc.new
+ super()
+ end
+
+ private
+
+ def visit(o)
+ super
+ @block.call o
+ end
+
+ def unary(o)
+ visit o.expr
+ end
+ alias :visit_Arel_Nodes_Else :unary
+ alias :visit_Arel_Nodes_Group :unary
+ alias :visit_Arel_Nodes_Cube :unary
+ alias :visit_Arel_Nodes_RollUp :unary
+ alias :visit_Arel_Nodes_GroupingSet :unary
+ alias :visit_Arel_Nodes_GroupingElement :unary
+ alias :visit_Arel_Nodes_Grouping :unary
+ alias :visit_Arel_Nodes_Having :unary
+ alias :visit_Arel_Nodes_Lateral :unary
+ alias :visit_Arel_Nodes_Limit :unary
+ alias :visit_Arel_Nodes_Not :unary
+ alias :visit_Arel_Nodes_Offset :unary
+ alias :visit_Arel_Nodes_On :unary
+ alias :visit_Arel_Nodes_Ordering :unary
+ alias :visit_Arel_Nodes_Ascending :unary
+ alias :visit_Arel_Nodes_Descending :unary
+ alias :visit_Arel_Nodes_Top :unary
+ alias :visit_Arel_Nodes_UnqualifiedColumn :unary
+
+ def function(o)
+ visit o.expressions
+ visit o.alias
+ visit o.distinct
+ end
+ alias :visit_Arel_Nodes_Avg :function
+ alias :visit_Arel_Nodes_Exists :function
+ alias :visit_Arel_Nodes_Max :function
+ alias :visit_Arel_Nodes_Min :function
+ alias :visit_Arel_Nodes_Sum :function
+
+ def visit_Arel_Nodes_NamedFunction(o)
+ visit o.name
+ visit o.expressions
+ visit o.distinct
+ visit o.alias
+ end
+
+ def visit_Arel_Nodes_Count(o)
+ visit o.expressions
+ visit o.alias
+ visit o.distinct
+ end
+
+ def visit_Arel_Nodes_Case(o)
+ visit o.case
+ visit o.conditions
+ visit o.default
+ end
+
+ def nary(o)
+ o.children.each { |child| visit child }
+ end
+ alias :visit_Arel_Nodes_And :nary
+
+ def binary(o)
+ visit o.left
+ visit o.right
+ end
+ alias :visit_Arel_Nodes_As :binary
+ alias :visit_Arel_Nodes_Assignment :binary
+ alias :visit_Arel_Nodes_Between :binary
+ alias :visit_Arel_Nodes_Concat :binary
+ alias :visit_Arel_Nodes_DeleteStatement :binary
+ alias :visit_Arel_Nodes_DoesNotMatch :binary
+ alias :visit_Arel_Nodes_Equality :binary
+ alias :visit_Arel_Nodes_FullOuterJoin :binary
+ alias :visit_Arel_Nodes_GreaterThan :binary
+ alias :visit_Arel_Nodes_GreaterThanOrEqual :binary
+ alias :visit_Arel_Nodes_In :binary
+ alias :visit_Arel_Nodes_InfixOperation :binary
+ alias :visit_Arel_Nodes_JoinSource :binary
+ alias :visit_Arel_Nodes_InnerJoin :binary
+ alias :visit_Arel_Nodes_LessThan :binary
+ alias :visit_Arel_Nodes_LessThanOrEqual :binary
+ alias :visit_Arel_Nodes_Matches :binary
+ alias :visit_Arel_Nodes_NotEqual :binary
+ alias :visit_Arel_Nodes_NotIn :binary
+ alias :visit_Arel_Nodes_NotRegexp :binary
+ alias :visit_Arel_Nodes_Or :binary
+ alias :visit_Arel_Nodes_OuterJoin :binary
+ alias :visit_Arel_Nodes_Regexp :binary
+ alias :visit_Arel_Nodes_RightOuterJoin :binary
+ alias :visit_Arel_Nodes_TableAlias :binary
+ alias :visit_Arel_Nodes_Values :binary
+ alias :visit_Arel_Nodes_When :binary
+
+ def visit_Arel_Nodes_StringJoin(o)
+ visit o.left
+ end
+
+ def visit_Arel_Attribute(o)
+ visit o.relation
+ visit o.name
+ end
+ alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Float :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_String :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Time :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Decimal :visit_Arel_Attribute
+
+ def visit_Arel_Table(o)
+ visit o.name
+ end
+
+ def terminal(o)
+ end
+ alias :visit_ActiveSupport_Multibyte_Chars :terminal
+ alias :visit_ActiveSupport_StringInquirer :terminal
+ alias :visit_Arel_Nodes_Lock :terminal
+ alias :visit_Arel_Nodes_Node :terminal
+ alias :visit_Arel_Nodes_SqlLiteral :terminal
+ alias :visit_Arel_Nodes_BindParam :terminal
+ alias :visit_Arel_Nodes_Window :terminal
+ alias :visit_Arel_Nodes_True :terminal
+ alias :visit_Arel_Nodes_False :terminal
+ alias :visit_BigDecimal :terminal
+ alias :visit_Bignum :terminal
+ alias :visit_Class :terminal
+ alias :visit_Date :terminal
+ alias :visit_DateTime :terminal
+ alias :visit_FalseClass :terminal
+ alias :visit_Fixnum :terminal
+ alias :visit_Float :terminal
+ alias :visit_Integer :terminal
+ alias :visit_NilClass :terminal
+ alias :visit_String :terminal
+ alias :visit_Symbol :terminal
+ alias :visit_Time :terminal
+ alias :visit_TrueClass :terminal
+
+ def visit_Arel_Nodes_InsertStatement(o)
+ visit o.relation
+ visit o.columns
+ visit o.values
+ end
+
+ def visit_Arel_Nodes_SelectCore(o)
+ visit o.projections
+ visit o.source
+ visit o.wheres
+ visit o.groups
+ visit o.windows
+ visit o.havings
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o)
+ visit o.cores
+ visit o.orders
+ visit o.limit
+ visit o.lock
+ visit o.offset
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o)
+ visit o.relation
+ visit o.values
+ visit o.wheres
+ visit o.orders
+ visit o.limit
+ end
+
+ def visit_Array(o)
+ o.each { |i| visit i }
+ end
+ alias :visit_Set :visit_Array
+
+ def visit_Hash(o)
+ o.each { |k, v| visit(k); visit(v) }
+ end
+
+ DISPATCH = dispatch_cache
+
+ def get_dispatch_cache
+ DISPATCH
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb
new file mode 100644
index 0000000000..d352b81914
--- /dev/null
+++ b/activerecord/lib/arel/visitors/dot.rb
@@ -0,0 +1,292 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Dot < Arel::Visitors::Visitor
+ class Node # :nodoc:
+ attr_accessor :name, :id, :fields
+
+ def initialize(name, id, fields = [])
+ @name = name
+ @id = id
+ @fields = fields
+ end
+ end
+
+ class Edge < Struct.new :name, :from, :to # :nodoc:
+ end
+
+ def initialize
+ super()
+ @nodes = []
+ @edges = []
+ @node_stack = []
+ @edge_stack = []
+ @seen = {}
+ end
+
+ def accept(object, collector)
+ visit object
+ collector << to_dot
+ end
+
+ private
+
+ def visit_Arel_Nodes_Ordering(o)
+ visit_edge o, "expr"
+ end
+
+ def visit_Arel_Nodes_TableAlias(o)
+ visit_edge o, "name"
+ visit_edge o, "relation"
+ end
+
+ def visit_Arel_Nodes_Count(o)
+ visit_edge o, "expressions"
+ visit_edge o, "distinct"
+ end
+
+ def visit_Arel_Nodes_Values(o)
+ visit_edge o, "expressions"
+ end
+
+ def visit_Arel_Nodes_StringJoin(o)
+ visit_edge o, "left"
+ end
+
+ def visit_Arel_Nodes_InnerJoin(o)
+ visit_edge o, "left"
+ visit_edge o, "right"
+ end
+ alias :visit_Arel_Nodes_FullOuterJoin :visit_Arel_Nodes_InnerJoin
+ alias :visit_Arel_Nodes_OuterJoin :visit_Arel_Nodes_InnerJoin
+ alias :visit_Arel_Nodes_RightOuterJoin :visit_Arel_Nodes_InnerJoin
+
+ def visit_Arel_Nodes_DeleteStatement(o)
+ visit_edge o, "relation"
+ visit_edge o, "wheres"
+ end
+
+ def unary(o)
+ visit_edge o, "expr"
+ end
+ alias :visit_Arel_Nodes_Group :unary
+ alias :visit_Arel_Nodes_Cube :unary
+ alias :visit_Arel_Nodes_RollUp :unary
+ alias :visit_Arel_Nodes_GroupingSet :unary
+ alias :visit_Arel_Nodes_GroupingElement :unary
+ alias :visit_Arel_Nodes_Grouping :unary
+ alias :visit_Arel_Nodes_Having :unary
+ alias :visit_Arel_Nodes_Limit :unary
+ alias :visit_Arel_Nodes_Not :unary
+ alias :visit_Arel_Nodes_Offset :unary
+ alias :visit_Arel_Nodes_On :unary
+ alias :visit_Arel_Nodes_Top :unary
+ alias :visit_Arel_Nodes_UnqualifiedColumn :unary
+ alias :visit_Arel_Nodes_Preceding :unary
+ alias :visit_Arel_Nodes_Following :unary
+ alias :visit_Arel_Nodes_Rows :unary
+ alias :visit_Arel_Nodes_Range :unary
+
+ def window(o)
+ visit_edge o, "partitions"
+ visit_edge o, "orders"
+ visit_edge o, "framing"
+ end
+ alias :visit_Arel_Nodes_Window :window
+
+ def named_window(o)
+ visit_edge o, "partitions"
+ visit_edge o, "orders"
+ visit_edge o, "framing"
+ visit_edge o, "name"
+ end
+ alias :visit_Arel_Nodes_NamedWindow :named_window
+
+ def function(o)
+ visit_edge o, "expressions"
+ visit_edge o, "distinct"
+ visit_edge o, "alias"
+ end
+ alias :visit_Arel_Nodes_Exists :function
+ alias :visit_Arel_Nodes_Min :function
+ alias :visit_Arel_Nodes_Max :function
+ alias :visit_Arel_Nodes_Avg :function
+ alias :visit_Arel_Nodes_Sum :function
+
+ def extract(o)
+ visit_edge o, "expressions"
+ visit_edge o, "alias"
+ end
+ alias :visit_Arel_Nodes_Extract :extract
+
+ def visit_Arel_Nodes_NamedFunction(o)
+ visit_edge o, "name"
+ visit_edge o, "expressions"
+ visit_edge o, "distinct"
+ visit_edge o, "alias"
+ end
+
+ def visit_Arel_Nodes_InsertStatement(o)
+ visit_edge o, "relation"
+ visit_edge o, "columns"
+ visit_edge o, "values"
+ end
+
+ def visit_Arel_Nodes_SelectCore(o)
+ visit_edge o, "source"
+ visit_edge o, "projections"
+ visit_edge o, "wheres"
+ visit_edge o, "windows"
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o)
+ visit_edge o, "cores"
+ visit_edge o, "limit"
+ visit_edge o, "orders"
+ visit_edge o, "offset"
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o)
+ visit_edge o, "relation"
+ visit_edge o, "wheres"
+ visit_edge o, "values"
+ end
+
+ def visit_Arel_Table(o)
+ visit_edge o, "name"
+ end
+
+ def visit_Arel_Nodes_Casted(o)
+ visit_edge o, "val"
+ visit_edge o, "attribute"
+ end
+
+ def visit_Arel_Attribute(o)
+ visit_edge o, "relation"
+ visit_edge o, "name"
+ end
+ alias :visit_Arel_Attributes_Integer :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Float :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_String :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Time :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Boolean :visit_Arel_Attribute
+ alias :visit_Arel_Attributes_Attribute :visit_Arel_Attribute
+
+ def nary(o)
+ o.children.each_with_index do |x, i|
+ edge(i) { visit x }
+ end
+ end
+ alias :visit_Arel_Nodes_And :nary
+
+ def binary(o)
+ visit_edge o, "left"
+ visit_edge o, "right"
+ end
+ alias :visit_Arel_Nodes_As :binary
+ alias :visit_Arel_Nodes_Assignment :binary
+ alias :visit_Arel_Nodes_Between :binary
+ alias :visit_Arel_Nodes_Concat :binary
+ alias :visit_Arel_Nodes_DoesNotMatch :binary
+ alias :visit_Arel_Nodes_Equality :binary
+ alias :visit_Arel_Nodes_GreaterThan :binary
+ alias :visit_Arel_Nodes_GreaterThanOrEqual :binary
+ alias :visit_Arel_Nodes_In :binary
+ alias :visit_Arel_Nodes_JoinSource :binary
+ alias :visit_Arel_Nodes_LessThan :binary
+ alias :visit_Arel_Nodes_LessThanOrEqual :binary
+ alias :visit_Arel_Nodes_Matches :binary
+ alias :visit_Arel_Nodes_NotEqual :binary
+ alias :visit_Arel_Nodes_NotIn :binary
+ alias :visit_Arel_Nodes_Or :binary
+ alias :visit_Arel_Nodes_Over :binary
+
+ def visit_String(o)
+ @node_stack.last.fields << o
+ end
+ alias :visit_Time :visit_String
+ alias :visit_Date :visit_String
+ alias :visit_DateTime :visit_String
+ alias :visit_NilClass :visit_String
+ alias :visit_TrueClass :visit_String
+ alias :visit_FalseClass :visit_String
+ alias :visit_Integer :visit_String
+ alias :visit_Fixnum :visit_String
+ alias :visit_BigDecimal :visit_String
+ alias :visit_Float :visit_String
+ alias :visit_Symbol :visit_String
+ alias :visit_Arel_Nodes_SqlLiteral :visit_String
+
+ def visit_Arel_Nodes_BindParam(o); end
+
+ def visit_Hash(o)
+ o.each_with_index do |pair, i|
+ edge("pair_#{i}") { visit pair }
+ end
+ end
+
+ def visit_Array(o)
+ o.each_with_index do |x, i|
+ edge(i) { visit x }
+ end
+ end
+ alias :visit_Set :visit_Array
+
+ def visit_edge(o, method)
+ edge(method) { visit o.send(method) }
+ end
+
+ def visit(o)
+ if node = @seen[o.object_id]
+ @edge_stack.last.to = node
+ return
+ end
+
+ node = Node.new(o.class.name, o.object_id)
+ @seen[node.id] = node
+ @nodes << node
+ with_node node do
+ super
+ end
+ end
+
+ def edge(name)
+ edge = Edge.new(name, @node_stack.last)
+ @edge_stack.push edge
+ @edges << edge
+ yield
+ @edge_stack.pop
+ end
+
+ def with_node(node)
+ if edge = @edge_stack.last
+ edge.to = node
+ end
+
+ @node_stack.push node
+ yield
+ @node_stack.pop
+ end
+
+ def quote(string)
+ string.to_s.gsub('"', '\"')
+ end
+
+ def to_dot
+ "digraph \"Arel\" {\nnode [width=0.375,height=0.25,shape=record];\n" +
+ @nodes.map { |node|
+ label = "<f0>#{node.name}"
+
+ node.fields.each_with_index do |field, i|
+ label += "|<f#{i + 1}>#{quote field}"
+ end
+
+ "#{node.id} [label=\"#{label}\"];"
+ }.join("\n") + "\n" + @edges.map { |edge|
+ "#{edge.from.id} -> #{edge.to.id} [label=\"#{edge.name}\"];"
+ }.join("\n") + "\n}"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/ibm_db.rb b/activerecord/lib/arel/visitors/ibm_db.rb
new file mode 100644
index 0000000000..0a06aef60b
--- /dev/null
+++ b/activerecord/lib/arel/visitors/ibm_db.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class IBM_DB < Arel::Visitors::ToSql
+ private
+
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector << "FETCH FIRST "
+ collector = visit o.expr, collector
+ collector << " ROWS ONLY"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/informix.rb b/activerecord/lib/arel/visitors/informix.rb
new file mode 100644
index 0000000000..0a9713794e
--- /dev/null
+++ b/activerecord/lib/arel/visitors/informix.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Informix < Arel::Visitors::ToSql
+ private
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ collector << "SELECT "
+ collector = maybe_visit o.offset, collector
+ collector = maybe_visit o.limit, collector
+ collector = o.cores.inject(collector) { |c, x|
+ visit_Arel_Nodes_SelectCore x, c
+ }
+ if o.orders.any?
+ collector << "ORDER BY "
+ collector = inject_join o.orders, collector, ", "
+ end
+ collector = maybe_visit o.lock, collector
+ end
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector = inject_join o.projections, collector, ", "
+ if o.source && !o.source.empty?
+ collector << " FROM "
+ collector = visit o.source, collector
+ end
+
+ if o.wheres.any?
+ collector << " WHERE "
+ collector = inject_join o.wheres, collector, " AND "
+ end
+
+ if o.groups.any?
+ collector << "GROUP BY "
+ collector = inject_join o.groups, collector, ", "
+ end
+
+ if o.havings.any?
+ collector << " HAVING "
+ collector = inject_join o.havings, collector, " AND "
+ end
+ collector
+ end
+
+ def visit_Arel_Nodes_Offset(o, collector)
+ collector << "SKIP "
+ visit o.expr, collector
+ end
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector << "FIRST "
+ visit o.expr, collector
+ collector << " "
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb
new file mode 100644
index 0000000000..9aedc51d15
--- /dev/null
+++ b/activerecord/lib/arel/visitors/mssql.rb
@@ -0,0 +1,125 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class MSSQL < Arel::Visitors::ToSql
+ RowNumber = Struct.new :children
+
+ def initialize(*)
+ @primary_keys = {}
+ super
+ end
+
+ private
+
+ # `top` wouldn't really work here. I.e. User.select("distinct first_name").limit(10) would generate
+ # "select top 10 distinct first_name from users", which is invalid query! it should be
+ # "select distinct top 10 first_name from users"
+ def visit_Arel_Nodes_Top(o)
+ ""
+ end
+
+ def visit_Arel_Visitors_MSSQL_RowNumber(o, collector)
+ collector << "ROW_NUMBER() OVER (ORDER BY "
+ inject_join(o.children, collector, ", ") << ") as _row_num"
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ if !o.limit && !o.offset
+ return super
+ end
+
+ is_select_count = false
+ o.cores.each { |x|
+ core_order_by = row_num_literal determine_order_by(o.orders, x)
+ if select_count? x
+ x.projections = [core_order_by]
+ is_select_count = true
+ else
+ x.projections << core_order_by
+ end
+ }
+
+ if is_select_count
+ # fixme count distinct wouldn't work with limit or offset
+ collector << "SELECT COUNT(1) as count_id FROM ("
+ end
+
+ collector << "SELECT _t.* FROM ("
+ collector = o.cores.inject(collector) { |c, x|
+ visit_Arel_Nodes_SelectCore x, c
+ }
+ collector << ") as _t WHERE #{get_offset_limit_clause(o)}"
+
+ if is_select_count
+ collector << ") AS subquery"
+ else
+ collector
+ end
+ end
+
+ def get_offset_limit_clause(o)
+ first_row = o.offset ? o.offset.expr.to_i + 1 : 1
+ last_row = o.limit ? o.limit.expr.to_i - 1 + first_row : nil
+ if last_row
+ " _row_num BETWEEN #{first_row} AND #{last_row}"
+ else
+ " _row_num >= #{first_row}"
+ end
+ end
+
+ def visit_Arel_Nodes_DeleteStatement(o, collector)
+ collector << "DELETE "
+ if o.limit
+ collector << "TOP ("
+ visit o.limit.expr, collector
+ collector << ") "
+ end
+ collector << "FROM "
+ collector = visit o.relation, collector
+ if o.wheres.any?
+ collector << " WHERE "
+ inject_join o.wheres, collector, AND
+ else
+ collector
+ end
+ end
+
+ def determine_order_by(orders, x)
+ if orders.any?
+ orders
+ elsif x.groups.any?
+ x.groups
+ else
+ pk = find_left_table_pk(x.froms)
+ pk ? [pk] : []
+ end
+ end
+
+ def row_num_literal(order_by)
+ RowNumber.new order_by
+ end
+
+ def select_count?(x)
+ x.projections.length == 1 && Arel::Nodes::Count === x.projections.first
+ end
+
+ # FIXME raise exception of there is no pk?
+ def find_left_table_pk(o)
+ if o.kind_of?(Arel::Nodes::Join)
+ find_left_table_pk(o.left)
+ elsif o.instance_of?(Arel::Table)
+ find_primary_key(o)
+ end
+ end
+
+ def find_primary_key(o)
+ @primary_keys[o.name] ||= begin
+ primary_key_name = @connection.primary_key(o.name)
+ # some tables might be without primary key
+ primary_key_name && o[primary_key_name]
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb
new file mode 100644
index 0000000000..37bfb661f0
--- /dev/null
+++ b/activerecord/lib/arel/visitors/mysql.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class MySQL < Arel::Visitors::ToSql
+ private
+ def visit_Arel_Nodes_Union(o, collector, suppress_parens = false)
+ unless suppress_parens
+ collector << "( "
+ end
+
+ collector = case o.left
+ when Arel::Nodes::Union
+ visit_Arel_Nodes_Union o.left, collector, true
+ else
+ visit o.left, collector
+ end
+
+ collector << " UNION "
+
+ collector = case o.right
+ when Arel::Nodes::Union
+ visit_Arel_Nodes_Union o.right, collector, true
+ else
+ visit o.right, collector
+ end
+
+ if suppress_parens
+ collector
+ else
+ collector << " )"
+ end
+ end
+
+ def visit_Arel_Nodes_Bin(o, collector)
+ collector << "BINARY "
+ visit o.expr, collector
+ end
+
+ ###
+ # :'(
+ # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ if o.offset && !o.limit
+ o.limit = Arel::Nodes::Limit.new(18446744073709551615)
+ end
+ super
+ end
+
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ o.froms ||= Arel.sql("DUAL")
+ super
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
+ collector << "UPDATE "
+ collector = visit o.relation, collector
+
+ unless o.values.empty?
+ collector << " SET "
+ collector = inject_join o.values, collector, ", "
+ end
+
+ unless o.wheres.empty?
+ collector << " WHERE "
+ collector = inject_join o.wheres, collector, " AND "
+ end
+
+ unless o.orders.empty?
+ collector << " ORDER BY "
+ collector = inject_join o.orders, collector, ", "
+ end
+
+ maybe_visit o.limit, collector
+ end
+
+ def visit_Arel_Nodes_Concat(o, collector)
+ collector << " CONCAT("
+ visit o.left, collector
+ collector << ", "
+ visit o.right, collector
+ collector << ") "
+ collector
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/oracle.rb b/activerecord/lib/arel/visitors/oracle.rb
new file mode 100644
index 0000000000..30a1529d46
--- /dev/null
+++ b/activerecord/lib/arel/visitors/oracle.rb
@@ -0,0 +1,153 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Oracle < Arel::Visitors::ToSql
+ private
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ o = order_hacks(o)
+
+ # if need to select first records without ORDER BY and GROUP BY and without DISTINCT
+ # then can use simple ROWNUM in WHERE clause
+ if o.limit && o.orders.empty? && o.cores.first.groups.empty? && !o.offset && o.cores.first.set_quantifier.class.to_s !~ /Distinct/
+ o.cores.last.wheres.push Nodes::LessThanOrEqual.new(
+ Nodes::SqlLiteral.new("ROWNUM"), o.limit.expr
+ )
+ return super
+ end
+
+ if o.limit && o.offset
+ o = o.dup
+ limit = o.limit.expr
+ offset = o.offset
+ o.offset = nil
+ collector << "
+ SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM ("
+
+ collector = super(o, collector)
+
+ if offset.expr.is_a? Nodes::BindParam
+ collector << ") raw_sql_ WHERE rownum <= ("
+ collector = visit offset.expr, collector
+ collector << " + "
+ collector = visit limit, collector
+ collector << ") ) WHERE raw_rnum_ > "
+ collector = visit offset.expr, collector
+ return collector
+ else
+ collector << ") raw_sql_
+ WHERE rownum <= #{offset.expr.to_i + limit}
+ )
+ WHERE "
+ return visit(offset, collector)
+ end
+ end
+
+ if o.limit
+ o = o.dup
+ limit = o.limit.expr
+ collector << "SELECT * FROM ("
+ collector = super(o, collector)
+ collector << ") WHERE ROWNUM <= "
+ return visit limit, collector
+ end
+
+ if o.offset
+ o = o.dup
+ offset = o.offset
+ o.offset = nil
+ collector << "SELECT * FROM (
+ SELECT raw_sql_.*, rownum raw_rnum_
+ FROM ("
+ collector = super(o, collector)
+ collector << ") raw_sql_
+ )
+ WHERE "
+ return visit offset, collector
+ end
+
+ super
+ end
+
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector
+ end
+
+ def visit_Arel_Nodes_Offset(o, collector)
+ collector << "raw_rnum_ > "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Except(o, collector)
+ collector << "( "
+ collector = infix_value o, collector, " MINUS "
+ collector << " )"
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
+ # Oracle does not allow ORDER BY/LIMIT in UPDATEs.
+ if o.orders.any? && o.limit.nil?
+ # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided,
+ # otherwise let the user deal with the error
+ o = o.dup
+ o.orders = []
+ end
+
+ super
+ end
+
+ ###
+ # Hacks for the order clauses specific to Oracle
+ def order_hacks(o)
+ return o if o.orders.empty?
+ return o unless o.cores.any? do |core|
+ core.projections.any? do |projection|
+ /FIRST_VALUE/ === projection
+ end
+ end
+ # Previous version with join and split broke ORDER BY clause
+ # if it contained functions with several arguments (separated by ',').
+ #
+ # orders = o.orders.map { |x| visit x }.join(', ').split(',')
+ orders = o.orders.map do |x|
+ string = visit(x, Arel::Collectors::SQLString.new).value
+ if string.include?(",")
+ split_order_string(string)
+ else
+ string
+ end
+ end.flatten
+ o.orders = []
+ orders.each_with_index do |order, i|
+ o.orders <<
+ Nodes::SqlLiteral.new("alias_#{i}__#{' DESC' if /\bdesc$/i === order}")
+ end
+ o
+ end
+
+ # Split string by commas but count opening and closing brackets
+ # and ignore commas inside brackets.
+ def split_order_string(string)
+ array = []
+ i = 0
+ string.split(",").each do |part|
+ if array[i]
+ array[i] << "," << part
+ else
+ # to ensure that array[i] will be String and not Arel::Nodes::SqlLiteral
+ array[i] = part.to_s
+ end
+ i += 1 if array[i].count("(") == array[i].count(")")
+ end
+ array
+ end
+
+ def visit_Arel_Nodes_BindParam(o, collector)
+ collector.add_bind(o.value) { |i| ":a#{i}" }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/oracle12.rb b/activerecord/lib/arel/visitors/oracle12.rb
new file mode 100644
index 0000000000..7061f06087
--- /dev/null
+++ b/activerecord/lib/arel/visitors/oracle12.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Oracle12 < Arel::Visitors::ToSql
+ private
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ # Oracle does not allow LIMIT clause with select for update
+ if o.limit && o.lock
+ raise ArgumentError, <<-MSG
+ 'Combination of limit and lock is not supported.
+ because generated SQL statements
+ `SELECT FOR UPDATE and FETCH FIRST n ROWS` generates ORA-02014.`
+ MSG
+ end
+ super
+ end
+
+ def visit_Arel_Nodes_SelectOptions(o, collector)
+ collector = maybe_visit o.offset, collector
+ collector = maybe_visit o.limit, collector
+ collector = maybe_visit o.lock, collector
+ end
+
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector << "FETCH FIRST "
+ collector = visit o.expr, collector
+ collector << " ROWS ONLY"
+ end
+
+ def visit_Arel_Nodes_Offset(o, collector)
+ collector << "OFFSET "
+ visit o.expr, collector
+ collector << " ROWS"
+ end
+
+ def visit_Arel_Nodes_Except(o, collector)
+ collector << "( "
+ collector = infix_value o, collector, " MINUS "
+ collector << " )"
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
+ # Oracle does not allow ORDER BY/LIMIT in UPDATEs.
+ if o.orders.any? && o.limit.nil?
+ # However, there is no harm in silently eating the ORDER BY clause if no LIMIT has been provided,
+ # otherwise let the user deal with the error
+ o = o.dup
+ o.orders = []
+ end
+
+ super
+ end
+
+ def visit_Arel_Nodes_BindParam(o, collector)
+ collector.add_bind(o.value) { |i| ":a#{i}" }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/postgresql.rb b/activerecord/lib/arel/visitors/postgresql.rb
new file mode 100644
index 0000000000..108ee431ee
--- /dev/null
+++ b/activerecord/lib/arel/visitors/postgresql.rb
@@ -0,0 +1,104 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class PostgreSQL < Arel::Visitors::ToSql
+ CUBE = "CUBE"
+ ROLLUP = "ROLLUP"
+ GROUPING_SET = "GROUPING SET"
+ LATERAL = "LATERAL"
+
+ private
+
+ def visit_Arel_Nodes_Matches(o, collector)
+ op = o.case_sensitive ? " LIKE " : " ILIKE "
+ collector = infix_value o, collector, op
+ if o.escape
+ collector << " ESCAPE "
+ visit o.escape, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_DoesNotMatch(o, collector)
+ op = o.case_sensitive ? " NOT LIKE " : " NOT ILIKE "
+ collector = infix_value o, collector, op
+ if o.escape
+ collector << " ESCAPE "
+ visit o.escape, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_Regexp(o, collector)
+ op = o.case_sensitive ? " ~ " : " ~* "
+ infix_value o, collector, op
+ end
+
+ def visit_Arel_Nodes_NotRegexp(o, collector)
+ op = o.case_sensitive ? " !~ " : " !~* "
+ infix_value o, collector, op
+ end
+
+ def visit_Arel_Nodes_DistinctOn(o, collector)
+ collector << "DISTINCT ON ( "
+ visit(o.expr, collector) << " )"
+ end
+
+ def visit_Arel_Nodes_BindParam(o, collector)
+ collector.add_bind(o.value) { |i| "$#{i}" }
+ end
+
+ def visit_Arel_Nodes_GroupingElement(o, collector)
+ collector << "( "
+ visit(o.expr, collector) << " )"
+ end
+
+ def visit_Arel_Nodes_Cube(o, collector)
+ collector << CUBE
+ grouping_array_or_grouping_element o, collector
+ end
+
+ def visit_Arel_Nodes_RollUp(o, collector)
+ collector << ROLLUP
+ grouping_array_or_grouping_element o, collector
+ end
+
+ def visit_Arel_Nodes_GroupingSet(o, collector)
+ collector << GROUPING_SET
+ grouping_array_or_grouping_element o, collector
+ end
+
+ def visit_Arel_Nodes_Lateral(o, collector)
+ collector << LATERAL
+ collector << SPACE
+ grouping_parentheses o, collector
+ end
+
+ # Used by Lateral visitor to enclose select queries in parentheses
+ def grouping_parentheses(o, collector)
+ if o.expr.is_a? Nodes::SelectStatement
+ collector << "("
+ visit o.expr, collector
+ collector << ")"
+ else
+ visit o.expr, collector
+ end
+ end
+
+ # Utilized by GroupingSet, Cube & RollUp visitors to
+ # handle grouping aggregation semantics
+ def grouping_array_or_grouping_element(o, collector)
+ if o.expr.is_a? Array
+ collector << "( "
+ visit o.expr, collector
+ collector << " )"
+ else
+ visit o.expr, collector
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/sqlite.rb b/activerecord/lib/arel/visitors/sqlite.rb
new file mode 100644
index 0000000000..cb1d2424ad
--- /dev/null
+++ b/activerecord/lib/arel/visitors/sqlite.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class SQLite < Arel::Visitors::ToSql
+ private
+
+ # Locks are not supported in SQLite
+ def visit_Arel_Nodes_Lock(o, collector)
+ collector
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ o.limit = Arel::Nodes::Limit.new(-1) if o.offset && !o.limit
+ super
+ end
+
+ def visit_Arel_Nodes_True(o, collector)
+ collector << "1"
+ end
+
+ def visit_Arel_Nodes_False(o, collector)
+ collector << "0"
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb
new file mode 100644
index 0000000000..5986fd5576
--- /dev/null
+++ b/activerecord/lib/arel/visitors/to_sql.rb
@@ -0,0 +1,847 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class UnsupportedVisitError < StandardError
+ def initialize(object)
+ super "Unsupported argument type: #{object.class.name}. Construct an Arel node instead."
+ end
+ end
+
+ class ToSql < Arel::Visitors::Visitor
+ ##
+ # This is some roflscale crazy stuff. I'm roflscaling this because
+ # building SQL queries is a hotspot. I will explain the roflscale so that
+ # others will not rm this code.
+ #
+ # In YARV, string literals in a method body will get duped when the byte
+ # code is executed. Let's take a look:
+ #
+ # > puts RubyVM::InstructionSequence.new('def foo; "bar"; end').disasm
+ #
+ # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>=====
+ # 0000 trace 8
+ # 0002 trace 1
+ # 0004 putstring "bar"
+ # 0006 trace 16
+ # 0008 leave
+ #
+ # The `putstring` bytecode will dup the string and push it on the stack.
+ # In many cases in our SQL visitor, that string is never mutated, so there
+ # is no need to dup the literal.
+ #
+ # If we change to a constant lookup, the string will not be duped, and we
+ # can reduce the objects in our system:
+ #
+ # > puts RubyVM::InstructionSequence.new('BAR = "bar"; def foo; BAR; end').disasm
+ #
+ # == disasm: <RubyVM::InstructionSequence:foo@<compiled>>========
+ # 0000 trace 8
+ # 0002 trace 1
+ # 0004 getinlinecache 11, <ic:0>
+ # 0007 getconstant :BAR
+ # 0009 setinlinecache <ic:0>
+ # 0011 trace 16
+ # 0013 leave
+ #
+ # `getconstant` should be a hash lookup, and no object is duped when the
+ # value of the constant is pushed on the stack. Hence the crazy
+ # constants below.
+ #
+ # `matches` and `doesNotMatch` operate case-insensitively via Visitor subclasses
+ # specialized for specific databases when necessary.
+ #
+
+ WHERE = " WHERE " # :nodoc:
+ SPACE = " " # :nodoc:
+ COMMA = ", " # :nodoc:
+ GROUP_BY = " GROUP BY " # :nodoc:
+ ORDER_BY = " ORDER BY " # :nodoc:
+ WINDOW = " WINDOW " # :nodoc:
+ AND = " AND " # :nodoc:
+
+ DISTINCT = "DISTINCT" # :nodoc:
+
+ def initialize(connection)
+ super()
+ @connection = connection
+ end
+
+ def compile(node, &block)
+ accept(node, Arel::Collectors::SQLString.new, &block).value
+ end
+
+ private
+
+ def visit_Arel_Nodes_DeleteStatement(o, collector)
+ collector << "DELETE FROM "
+ collector = visit o.relation, collector
+ if o.wheres.any?
+ collector << WHERE
+ collector = inject_join o.wheres, collector, AND
+ end
+
+ maybe_visit o.limit, collector
+ end
+
+ # FIXME: we should probably have a 2-pass visitor for this
+ def build_subselect(key, o)
+ stmt = Nodes::SelectStatement.new
+ core = stmt.cores.first
+ core.froms = o.relation
+ core.wheres = o.wheres
+ core.projections = [key]
+ stmt.limit = o.limit
+ stmt.orders = o.orders
+ stmt
+ end
+
+ def visit_Arel_Nodes_UpdateStatement(o, collector)
+ if o.orders.empty? && o.limit.nil?
+ wheres = o.wheres
+ else
+ wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])]
+ end
+
+ collector << "UPDATE "
+ collector = visit o.relation, collector
+ unless o.values.empty?
+ collector << " SET "
+ collector = inject_join o.values, collector, ", "
+ end
+
+ unless wheres.empty?
+ collector << " WHERE "
+ collector = inject_join wheres, collector, " AND "
+ end
+
+ collector
+ end
+
+ def visit_Arel_Nodes_InsertStatement(o, collector)
+ collector << "INSERT INTO "
+ collector = visit o.relation, collector
+ if o.columns.any?
+ collector << " (#{o.columns.map { |x|
+ quote_column_name x.name
+ }.join ', '})"
+ end
+
+ if o.values
+ maybe_visit o.values, collector
+ elsif o.select
+ maybe_visit o.select, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_Exists(o, collector)
+ collector << "EXISTS ("
+ collector = visit(o.expressions, collector) << ")"
+ if o.alias
+ collector << " AS "
+ visit o.alias, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_Casted(o, collector)
+ collector << quoted(o.val, o.attribute).to_s
+ end
+
+ def visit_Arel_Nodes_Quoted(o, collector)
+ collector << quoted(o.expr, nil).to_s
+ end
+
+ def visit_Arel_Nodes_True(o, collector)
+ collector << "TRUE"
+ end
+
+ def visit_Arel_Nodes_False(o, collector)
+ collector << "FALSE"
+ end
+
+ def visit_Arel_Nodes_ValuesList(o, collector)
+ collector << "VALUES "
+
+ len = o.rows.length - 1
+ o.rows.each_with_index { |row, i|
+ collector << "("
+ row_len = row.length - 1
+ row.each_with_index do |value, k|
+ case value
+ when Nodes::SqlLiteral, Nodes::BindParam
+ collector = visit(value, collector)
+ else
+ collector << quote(value)
+ end
+ collector << COMMA unless k == row_len
+ end
+ collector << ")"
+ collector << COMMA unless i == len
+ }
+ collector
+ end
+
+ def visit_Arel_Nodes_Values(o, collector)
+ collector << "VALUES ("
+
+ len = o.expressions.length - 1
+ o.expressions.each_with_index { |value, i|
+ case value
+ when Nodes::SqlLiteral, Nodes::BindParam
+ collector = visit value, collector
+ else
+ collector << quote(value).to_s
+ end
+ unless i == len
+ collector << COMMA
+ end
+ }
+
+ collector << ")"
+ end
+
+ def visit_Arel_Nodes_SelectStatement(o, collector)
+ if o.with
+ collector = visit o.with, collector
+ collector << SPACE
+ end
+
+ collector = o.cores.inject(collector) { |c, x|
+ visit_Arel_Nodes_SelectCore(x, c)
+ }
+
+ unless o.orders.empty?
+ collector << ORDER_BY
+ len = o.orders.length - 1
+ o.orders.each_with_index { |x, i|
+ collector = visit(x, collector)
+ collector << COMMA unless len == i
+ }
+ end
+
+ visit_Arel_Nodes_SelectOptions(o, collector)
+
+ collector
+ end
+
+ def visit_Arel_Nodes_SelectOptions(o, collector)
+ collector = maybe_visit o.limit, collector
+ collector = maybe_visit o.offset, collector
+ collector = maybe_visit o.lock, collector
+ end
+
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector << "SELECT"
+
+ collector = maybe_visit o.top, collector
+
+ collector = maybe_visit o.set_quantifier, collector
+
+ collect_nodes_for o.projections, collector, SPACE
+
+ if o.source && !o.source.empty?
+ collector << " FROM "
+ collector = visit o.source, collector
+ end
+
+ collect_nodes_for o.wheres, collector, WHERE, AND
+ collect_nodes_for o.groups, collector, GROUP_BY
+ unless o.havings.empty?
+ collector << " HAVING "
+ inject_join o.havings, collector, AND
+ end
+ collect_nodes_for o.windows, collector, WINDOW
+
+ collector
+ end
+
+ def collect_nodes_for(nodes, collector, spacer, connector = COMMA)
+ unless nodes.empty?
+ collector << spacer
+ len = nodes.length - 1
+ nodes.each_with_index do |x, i|
+ collector = visit(x, collector)
+ collector << connector unless len == i
+ end
+ end
+ end
+
+ def visit_Arel_Nodes_Bin(o, collector)
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Distinct(o, collector)
+ collector << DISTINCT
+ end
+
+ def visit_Arel_Nodes_DistinctOn(o, collector)
+ raise NotImplementedError, "DISTINCT ON not implemented for this db"
+ end
+
+ def visit_Arel_Nodes_With(o, collector)
+ collector << "WITH "
+ inject_join o.children, collector, COMMA
+ end
+
+ def visit_Arel_Nodes_WithRecursive(o, collector)
+ collector << "WITH RECURSIVE "
+ inject_join o.children, collector, COMMA
+ end
+
+ def visit_Arel_Nodes_Union(o, collector)
+ collector << "( "
+ infix_value(o, collector, " UNION ") << " )"
+ end
+
+ def visit_Arel_Nodes_UnionAll(o, collector)
+ collector << "( "
+ infix_value(o, collector, " UNION ALL ") << " )"
+ end
+
+ def visit_Arel_Nodes_Intersect(o, collector)
+ collector << "( "
+ infix_value(o, collector, " INTERSECT ") << " )"
+ end
+
+ def visit_Arel_Nodes_Except(o, collector)
+ collector << "( "
+ infix_value(o, collector, " EXCEPT ") << " )"
+ end
+
+ def visit_Arel_Nodes_NamedWindow(o, collector)
+ collector << quote_column_name(o.name)
+ collector << " AS "
+ visit_Arel_Nodes_Window o, collector
+ end
+
+ def visit_Arel_Nodes_Window(o, collector)
+ collector << "("
+
+ if o.partitions.any?
+ collector << "PARTITION BY "
+ collector = inject_join o.partitions, collector, ", "
+ end
+
+ if o.orders.any?
+ collector << SPACE if o.partitions.any?
+ collector << "ORDER BY "
+ collector = inject_join o.orders, collector, ", "
+ end
+
+ if o.framing
+ collector << SPACE if o.partitions.any? || o.orders.any?
+ collector = visit o.framing, collector
+ end
+
+ collector << ")"
+ end
+
+ def visit_Arel_Nodes_Rows(o, collector)
+ if o.expr
+ collector << "ROWS "
+ visit o.expr, collector
+ else
+ collector << "ROWS"
+ end
+ end
+
+ def visit_Arel_Nodes_Range(o, collector)
+ if o.expr
+ collector << "RANGE "
+ visit o.expr, collector
+ else
+ collector << "RANGE"
+ end
+ end
+
+ def visit_Arel_Nodes_Preceding(o, collector)
+ collector = if o.expr
+ visit o.expr, collector
+ else
+ collector << "UNBOUNDED"
+ end
+
+ collector << " PRECEDING"
+ end
+
+ def visit_Arel_Nodes_Following(o, collector)
+ collector = if o.expr
+ visit o.expr, collector
+ else
+ collector << "UNBOUNDED"
+ end
+
+ collector << " FOLLOWING"
+ end
+
+ def visit_Arel_Nodes_CurrentRow(o, collector)
+ collector << "CURRENT ROW"
+ end
+
+ def visit_Arel_Nodes_Over(o, collector)
+ case o.right
+ when nil
+ visit(o.left, collector) << " OVER ()"
+ when Arel::Nodes::SqlLiteral
+ infix_value o, collector, " OVER "
+ when String, Symbol
+ visit(o.left, collector) << " OVER #{quote_column_name o.right.to_s}"
+ else
+ infix_value o, collector, " OVER "
+ end
+ end
+
+ def visit_Arel_Nodes_Offset(o, collector)
+ collector << "OFFSET "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Limit(o, collector)
+ collector << "LIMIT "
+ visit o.expr, collector
+ end
+
+ # FIXME: this does nothing on most databases, but does on MSSQL
+ def visit_Arel_Nodes_Top(o, collector)
+ collector
+ end
+
+ def visit_Arel_Nodes_Lock(o, collector)
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Grouping(o, collector)
+ if o.expr.is_a? Nodes::Grouping
+ visit(o.expr, collector)
+ else
+ collector << "("
+ visit(o.expr, collector) << ")"
+ end
+ end
+
+ def visit_Arel_SelectManager(o, collector)
+ collector << "("
+ visit(o.ast, collector) << ")"
+ end
+
+ def visit_Arel_Nodes_Ascending(o, collector)
+ visit(o.expr, collector) << " ASC"
+ end
+
+ def visit_Arel_Nodes_Descending(o, collector)
+ visit(o.expr, collector) << " DESC"
+ end
+
+ def visit_Arel_Nodes_Group(o, collector)
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_NamedFunction(o, collector)
+ collector << o.name
+ collector << "("
+ collector << "DISTINCT " if o.distinct
+ collector = inject_join(o.expressions, collector, ", ") << ")"
+ if o.alias
+ collector << " AS "
+ visit o.alias, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_Extract(o, collector)
+ collector << "EXTRACT(#{o.field.to_s.upcase} FROM "
+ visit(o.expr, collector) << ")"
+ end
+
+ def visit_Arel_Nodes_Count(o, collector)
+ aggregate "COUNT", o, collector
+ end
+
+ def visit_Arel_Nodes_Sum(o, collector)
+ aggregate "SUM", o, collector
+ end
+
+ def visit_Arel_Nodes_Max(o, collector)
+ aggregate "MAX", o, collector
+ end
+
+ def visit_Arel_Nodes_Min(o, collector)
+ aggregate "MIN", o, collector
+ end
+
+ def visit_Arel_Nodes_Avg(o, collector)
+ aggregate "AVG", o, collector
+ end
+
+ def visit_Arel_Nodes_TableAlias(o, collector)
+ collector = visit o.relation, collector
+ collector << " "
+ collector << quote_table_name(o.name)
+ end
+
+ def visit_Arel_Nodes_Between(o, collector)
+ collector = visit o.left, collector
+ collector << " BETWEEN "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_GreaterThanOrEqual(o, collector)
+ collector = visit o.left, collector
+ collector << " >= "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_GreaterThan(o, collector)
+ collector = visit o.left, collector
+ collector << " > "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_LessThanOrEqual(o, collector)
+ collector = visit o.left, collector
+ collector << " <= "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_LessThan(o, collector)
+ collector = visit o.left, collector
+ collector << " < "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_Matches(o, collector)
+ collector = visit o.left, collector
+ collector << " LIKE "
+ collector = visit o.right, collector
+ if o.escape
+ collector << " ESCAPE "
+ visit o.escape, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_DoesNotMatch(o, collector)
+ collector = visit o.left, collector
+ collector << " NOT LIKE "
+ collector = visit o.right, collector
+ if o.escape
+ collector << " ESCAPE "
+ visit o.escape, collector
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_JoinSource(o, collector)
+ if o.left
+ collector = visit o.left, collector
+ end
+ if o.right.any?
+ collector << SPACE if o.left
+ collector = inject_join o.right, collector, SPACE
+ end
+ collector
+ end
+
+ def visit_Arel_Nodes_Regexp(o, collector)
+ raise NotImplementedError, "~ not implemented for this db"
+ end
+
+ def visit_Arel_Nodes_NotRegexp(o, collector)
+ raise NotImplementedError, "!~ not implemented for this db"
+ end
+
+ def visit_Arel_Nodes_StringJoin(o, collector)
+ visit o.left, collector
+ end
+
+ def visit_Arel_Nodes_FullOuterJoin(o, collector)
+ collector << "FULL OUTER JOIN "
+ collector = visit o.left, collector
+ collector << SPACE
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_OuterJoin(o, collector)
+ collector << "LEFT OUTER JOIN "
+ collector = visit o.left, collector
+ collector << " "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_RightOuterJoin(o, collector)
+ collector << "RIGHT OUTER JOIN "
+ collector = visit o.left, collector
+ collector << SPACE
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_InnerJoin(o, collector)
+ collector << "INNER JOIN "
+ collector = visit o.left, collector
+ if o.right
+ collector << SPACE
+ visit(o.right, collector)
+ else
+ collector
+ end
+ end
+
+ def visit_Arel_Nodes_On(o, collector)
+ collector << "ON "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_Not(o, collector)
+ collector << "NOT ("
+ visit(o.expr, collector) << ")"
+ end
+
+ def visit_Arel_Table(o, collector)
+ if o.table_alias
+ collector << "#{quote_table_name o.name} #{quote_table_name o.table_alias}"
+ else
+ collector << quote_table_name(o.name)
+ end
+ end
+
+ def visit_Arel_Nodes_In(o, collector)
+ if Array === o.right && o.right.empty?
+ collector << "1=0"
+ else
+ collector = visit o.left, collector
+ collector << " IN ("
+ visit(o.right, collector) << ")"
+ end
+ end
+
+ def visit_Arel_Nodes_NotIn(o, collector)
+ if Array === o.right && o.right.empty?
+ collector << "1=1"
+ else
+ collector = visit o.left, collector
+ collector << " NOT IN ("
+ collector = visit o.right, collector
+ collector << ")"
+ end
+ end
+
+ def visit_Arel_Nodes_And(o, collector)
+ inject_join o.children, collector, " AND "
+ end
+
+ def visit_Arel_Nodes_Or(o, collector)
+ collector = visit o.left, collector
+ collector << " OR "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_Assignment(o, collector)
+ case o.right
+ when Arel::Nodes::UnqualifiedColumn, Arel::Attributes::Attribute, Arel::Nodes::BindParam
+ collector = visit o.left, collector
+ collector << " = "
+ visit o.right, collector
+ else
+ collector = visit o.left, collector
+ collector << " = "
+ collector << quote(o.right).to_s
+ end
+ end
+
+ def visit_Arel_Nodes_Equality(o, collector)
+ right = o.right
+
+ collector = visit o.left, collector
+
+ if right.nil?
+ collector << " IS NULL"
+ else
+ collector << " = "
+ visit right, collector
+ end
+ end
+
+ def visit_Arel_Nodes_NotEqual(o, collector)
+ right = o.right
+
+ collector = visit o.left, collector
+
+ if right.nil?
+ collector << " IS NOT NULL"
+ else
+ collector << " != "
+ visit right, collector
+ end
+ end
+
+ def visit_Arel_Nodes_As(o, collector)
+ collector = visit o.left, collector
+ collector << " AS "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_Case(o, collector)
+ collector << "CASE "
+ if o.case
+ visit o.case, collector
+ collector << " "
+ end
+ o.conditions.each do |condition|
+ visit condition, collector
+ collector << " "
+ end
+ if o.default
+ visit o.default, collector
+ collector << " "
+ end
+ collector << "END"
+ end
+
+ def visit_Arel_Nodes_When(o, collector)
+ collector << "WHEN "
+ visit o.left, collector
+ collector << " THEN "
+ visit o.right, collector
+ end
+
+ def visit_Arel_Nodes_Else(o, collector)
+ collector << "ELSE "
+ visit o.expr, collector
+ end
+
+ def visit_Arel_Nodes_UnqualifiedColumn(o, collector)
+ collector << "#{quote_column_name o.name}"
+ collector
+ end
+
+ def visit_Arel_Attributes_Attribute(o, collector)
+ join_name = o.relation.table_alias || o.relation.name
+ collector << "#{quote_table_name join_name}.#{quote_column_name o.name}"
+ end
+ alias :visit_Arel_Attributes_Integer :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_Float :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_Decimal :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_String :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_Time :visit_Arel_Attributes_Attribute
+ alias :visit_Arel_Attributes_Boolean :visit_Arel_Attributes_Attribute
+
+ def literal(o, collector); collector << o.to_s; end
+
+ def visit_Arel_Nodes_BindParam(o, collector)
+ collector.add_bind(o.value) { "?" }
+ end
+
+ alias :visit_Arel_Nodes_SqlLiteral :literal
+ alias :visit_Bignum :literal
+ alias :visit_Fixnum :literal
+ alias :visit_Integer :literal
+
+ def quoted(o, a)
+ if a && a.able_to_type_cast?
+ quote(a.type_cast_for_database(o))
+ else
+ quote(o)
+ end
+ end
+
+ def unsupported(o, collector)
+ raise UnsupportedVisitError.new(o)
+ end
+
+ alias :visit_ActiveSupport_Multibyte_Chars :unsupported
+ alias :visit_ActiveSupport_StringInquirer :unsupported
+ alias :visit_BigDecimal :unsupported
+ alias :visit_Class :unsupported
+ alias :visit_Date :unsupported
+ alias :visit_DateTime :unsupported
+ alias :visit_FalseClass :unsupported
+ alias :visit_Float :unsupported
+ alias :visit_Hash :unsupported
+ alias :visit_NilClass :unsupported
+ alias :visit_String :unsupported
+ alias :visit_Symbol :unsupported
+ alias :visit_Time :unsupported
+ alias :visit_TrueClass :unsupported
+
+ def visit_Arel_Nodes_InfixOperation(o, collector)
+ collector = visit o.left, collector
+ collector << " #{o.operator} "
+ visit o.right, collector
+ end
+
+ alias :visit_Arel_Nodes_Addition :visit_Arel_Nodes_InfixOperation
+ alias :visit_Arel_Nodes_Subtraction :visit_Arel_Nodes_InfixOperation
+ alias :visit_Arel_Nodes_Multiplication :visit_Arel_Nodes_InfixOperation
+ alias :visit_Arel_Nodes_Division :visit_Arel_Nodes_InfixOperation
+
+ def visit_Arel_Nodes_UnaryOperation(o, collector)
+ collector << " #{o.operator} "
+ visit o.expr, collector
+ end
+
+ def visit_Array(o, collector)
+ inject_join o, collector, ", "
+ end
+ alias :visit_Set :visit_Array
+
+ def quote(value)
+ return value if Arel::Nodes::SqlLiteral === value
+ @connection.quote value
+ end
+
+ def quote_table_name(name)
+ return name if Arel::Nodes::SqlLiteral === name
+ @connection.quote_table_name(name)
+ end
+
+ def quote_column_name(name)
+ return name if Arel::Nodes::SqlLiteral === name
+ @connection.quote_column_name(name)
+ end
+
+ def maybe_visit(thing, collector)
+ return collector unless thing
+ collector << " "
+ visit thing, collector
+ end
+
+ def inject_join(list, collector, join_str)
+ len = list.length - 1
+ list.each_with_index.inject(collector) { |c, (x, i)|
+ if i == len
+ visit x, c
+ else
+ visit(x, c) << join_str
+ end
+ }
+ end
+
+ def infix_value(o, collector, value)
+ collector = visit o.left, collector
+ collector << value
+ visit o.right, collector
+ end
+
+ def aggregate(name, o, collector)
+ collector << "#{name}("
+ if o.distinct
+ collector << "DISTINCT "
+ end
+ collector = inject_join(o.expressions, collector, ", ") << ")"
+ if o.alias
+ collector << " AS "
+ visit o.alias, collector
+ else
+ collector
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/visitor.rb b/activerecord/lib/arel/visitors/visitor.rb
new file mode 100644
index 0000000000..1c17184e86
--- /dev/null
+++ b/activerecord/lib/arel/visitors/visitor.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class Visitor
+ def initialize
+ @dispatch = get_dispatch_cache
+ end
+
+ def accept(object, *args)
+ visit object, *args
+ end
+
+ private
+
+ attr_reader :dispatch
+
+ def self.dispatch_cache
+ Hash.new do |hash, klass|
+ hash[klass] = "visit_#{(klass.name || '').gsub('::', '_')}"
+ end
+ end
+
+ def get_dispatch_cache
+ self.class.dispatch_cache
+ end
+
+ def visit(object, *args)
+ dispatch_method = dispatch[object.class]
+ send dispatch_method, object, *args
+ rescue NoMethodError => e
+ raise e if respond_to?(dispatch_method, true)
+ superklass = object.class.ancestors.find { |klass|
+ respond_to?(dispatch[klass], true)
+ }
+ raise(TypeError, "Cannot visit #{object.class}") unless superklass
+ dispatch[object.class] = dispatch[superklass]
+ retry
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/visitors/where_sql.rb b/activerecord/lib/arel/visitors/where_sql.rb
new file mode 100644
index 0000000000..c6caf5e7c9
--- /dev/null
+++ b/activerecord/lib/arel/visitors/where_sql.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module Visitors
+ class WhereSql < Arel::Visitors::ToSql
+ def initialize(inner_visitor, *args, &block)
+ @inner_visitor = inner_visitor
+ super(*args, &block)
+ end
+
+ private
+
+ def visit_Arel_Nodes_SelectCore(o, collector)
+ collector << "WHERE "
+ wheres = o.wheres.map do |where|
+ Nodes::SqlLiteral.new(@inner_visitor.accept(where, collector.class.new).value)
+ end
+
+ inject_join wheres, collector, " AND "
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/arel/window_predications.rb b/activerecord/lib/arel/window_predications.rb
new file mode 100644
index 0000000000..3a8ee41f8a
--- /dev/null
+++ b/activerecord/lib/arel/window_predications.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Arel # :nodoc: all
+ module WindowPredications
+ def over(expr = nil)
+ Nodes::Over.new(self, expr)
+ end
+ end
+end
diff --git a/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb b/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt
index 60050e0bf8..60050e0bf8 100644
--- a/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb
+++ b/activerecord/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt
diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
index 856fcc5897..a07b00ef79 100644
--- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -15,12 +15,8 @@ module ActiveRecord
migration_template @migration_template, File.join(db_migrate_path, "#{file_name}.rb")
end
- # TODO Change this to private once we've dropped Ruby 2.2 support.
- # Workaround for Ruby 2.2 "private attribute?" warning.
- protected
- attr_reader :migration_action, :join_tables
-
private
+ attr_reader :migration_action, :join_tables
# Sets the default migration template that is being used for the generation of the migration.
# Depending on command line arguments, the migration template and the table name instance
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt
index 5f7201cfe1..5f7201cfe1 100644
--- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb.tt
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt
index 481c70201b..481c70201b 100644
--- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb.tt
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt
index 55dc65c8ad..55dc65c8ad 100644
--- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb
+++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb.tt
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/module.rb b/activerecord/lib/rails/generators/active_record/model/templates/module.rb.tt
index a3bf1c37b6..a3bf1c37b6 100644
--- a/activerecord/lib/rails/generators/active_record/model/templates/module.rb
+++ b/activerecord/lib/rails/generators/active_record/model/templates/module.rb.tt
diff --git a/activerecord/test/.gitignore b/activerecord/test/.gitignore
deleted file mode 100644
index a0ec5967dd..0000000000
--- a/activerecord/test/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-/config.yml
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 9aaa2852d0..02c7e47583 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -20,7 +20,7 @@ module ActiveRecord
b = Book.create(name: "my \x00 book")
b.reload
assert_equal "my \x00 book", b.name
- b.update_attributes(name: "my other \x00 book")
+ b.update(name: "my other \x00 book")
b.reload
assert_equal "my other \x00 book", b.name
end
@@ -78,13 +78,13 @@ module ActiveRecord
idx_name = "accounts_idx"
indexes = @connection.indexes("accounts")
- assert indexes.empty?
+ assert_empty indexes
@connection.add_index :accounts, :firm_id, name: idx_name
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
@@ -368,16 +368,16 @@ module ActiveRecord
unless in_memory_db?
test "transaction state is reset after a reconnect" do
@connection.begin_transaction
- assert @connection.transaction_open?
+ assert_predicate @connection, :transaction_open?
@connection.reconnect!
- assert !@connection.transaction_open?
+ assert_not_predicate @connection, :transaction_open?
end
test "transaction state is reset after a disconnect" do
@connection.begin_transaction
- assert @connection.transaction_open?
+ assert_predicate @connection, :transaction_open?
@connection.disconnect!
- assert !@connection.transaction_open?
+ assert_not_predicate @connection, :transaction_open?
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 a6b83ec377..976c5dde58 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -9,7 +9,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
def setup
ActiveRecord::Base.connection.singleton_class.class_eval do
alias_method :execute_without_stub, :execute
- def execute(sql, name = nil) return sql end
+ def execute(sql, name = nil) sql end
end
end
@@ -68,14 +68,14 @@ 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`)) ENGINE=InnoDB"
+ expected = "CREATE 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
end
- expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10))) ENGINE=InnoDB"
+ expected = "CREATE 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
@@ -108,7 +108,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
def test_create_mysql_database_with_encoding
assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, charset: "latin1")
- assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, charset: :big5, collation: :big5_chinese_ci)
+ assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT COLLATE `utf8mb4_bin`", create_database(:matt_aimonetti, collation: "utf8mb4_bin")
end
def test_recreate_mysql_database_with_encoding
@@ -148,8 +148,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
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")
+ assert_not column_present?("delete_me", "updated_at", "datetime")
+ assert_not column_present?("delete_me", "created_at", "datetime")
ensure
ActiveRecord::Base.connection.drop_table :delete_me rescue nil
end
@@ -160,7 +160,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase
ActiveRecord::Base.connection.stubs(:data_source_exists?).with(:temp).returns(false)
ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false)
- expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`)) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query"
+ expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`)) AS SELECT id, name, zip FROM a_really_complicated_query"
actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t|
t.index :zip
end
diff --git a/activerecord/test/cases/adapters/mysql2/auto_increment_test.rb b/activerecord/test/cases/adapters/mysql2/auto_increment_test.rb
new file mode 100644
index 0000000000..4c67633946
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/auto_increment_test.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "support/schema_dumping_helper"
+
+class Mysql2AutoIncrementTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ @connection.drop_table :auto_increments, if_exists: true
+ end
+
+ def test_auto_increment_without_primary_key
+ @connection.create_table :auto_increments, id: false, force: true do |t|
+ t.integer :id, null: false, auto_increment: true
+ t.index :id
+ end
+ output = dump_table_schema("auto_increments")
+ assert_match(/t\.integer\s+"id",\s+null: false,\s+auto_increment: true$/, output)
+ end
+
+ def test_auto_increment_with_composite_primary_key
+ @connection.create_table :auto_increments, primary_key: [:id, :created_at], force: true do |t|
+ t.integer :id, null: false, auto_increment: true
+ t.datetime :created_at, null: false
+ end
+ output = dump_table_schema("auto_increments")
+ assert_match(/t\.integer\s+"id",\s+null: false,\s+auto_increment: true$/, output)
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
index fd5f712f1a..aa870349be 100644
--- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb
@@ -14,8 +14,8 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase
end
def test_case_sensitive
- assert !CollationTest.columns_hash["string_ci_column"].case_sensitive?
- assert CollationTest.columns_hash["string_cs_column"].case_sensitive?
+ assert_not_predicate CollationTest.columns_hash["string_ci_column"], :case_sensitive?
+ assert_predicate CollationTest.columns_hash["string_cs_column"], :case_sensitive?
end
def test_case_insensitive_comparison_for_ci_column
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index 9d81d506a0..726f58d58e 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -40,41 +40,29 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
end
def test_no_automatic_reconnection_after_timeout
- assert @connection.active?
+ assert_predicate @connection, :active?
@connection.update("set @@wait_timeout=1")
sleep 2
- assert !@connection.active?
+ assert_not_predicate @connection, :active?
ensure
# Repair all fixture connections so other tests won't break.
@fixture_connections.each(&:verify!)
end
def test_successful_reconnection_after_timeout_with_manual_reconnect
- assert @connection.active?
+ assert_predicate @connection, :active?
@connection.update("set @@wait_timeout=1")
sleep 2
@connection.reconnect!
- assert @connection.active?
+ assert_predicate @connection, :active?
end
def test_successful_reconnection_after_timeout_with_verify
- assert @connection.active?
+ assert_predicate @connection, :active?
@connection.update("set @@wait_timeout=1")
sleep 2
@connection.verify!
- assert @connection.active?
- end
-
- def test_verify_with_args_is_deprecated
- assert_deprecated do
- @connection.verify!(option: true)
- end
- assert_deprecated do
- @connection.verify!([])
- end
- assert_deprecated do
- @connection.verify!({})
- end
+ assert_predicate @connection, :active?
end
def test_execute_after_disconnect
@@ -186,10 +174,10 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase
assert_equal "SCHEMA", @subscriber.logged[0][1]
end
- def test_logs_name_rename_column_sql
+ def test_logs_name_rename_column_for_alter
@connection.execute "CREATE TABLE `bar_baz` (`foo` varchar(255))"
@subscriber.logged.clear
- @connection.send(:rename_column_sql, "bar_baz", "foo", "foo2")
+ @connection.send(:rename_column_for_alter, "bar_baz", "foo", "foo2")
assert_equal "SCHEMA", @subscriber.logged[0][1]
ensure
@connection.execute "DROP TABLE `bar_baz`"
diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb
index 108bec832c..832f5d61d1 100644
--- a/activerecord/test/cases/adapters/mysql2/enum_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb
@@ -13,11 +13,11 @@ class Mysql2EnumTest < ActiveRecord::Mysql2TestCase
def test_should_not_be_unsigned
column = EnumTest.columns_hash["enum_column"]
- assert_not column.unsigned?
+ assert_not_predicate column, :unsigned?
end
def test_should_not_be_bigint
column = EnumTest.columns_hash["enum_column"]
- assert_not column.bigint?
+ assert_not_predicate column, :bigint?
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb
index 2736f7cf0e..b8e778f0b0 100644
--- a/activerecord/test/cases/adapters/mysql2/explain_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb
@@ -1,23 +1,23 @@
# frozen_string_literal: true
require "cases/helper"
-require "models/developer"
-require "models/computer"
+require "models/author"
+require "models/post"
class Mysql2ExplainTest < ActiveRecord::Mysql2TestCase
- fixtures :developers
+ fixtures :authors
def test_explain_for_one_query
- explain = Developer.where(id: 1).explain
- assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
- assert_match %r(developers |.* const), explain
+ explain = Author.where(id: 1).explain
+ assert_match %(EXPLAIN for: SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1), explain
+ assert_match %r(authors |.* const), explain
end
def test_explain_with_eager_loading
- explain = Developer.where(id: 1).includes(:audit_logs).explain
- assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain
- assert_match %r(developers |.* const), explain
- assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain
- assert_match %r(audit_logs |.* ALL), explain
+ explain = Author.where(id: 1).includes(:posts).explain
+ assert_match %(EXPLAIN for: SELECT `authors`.* FROM `authors` WHERE `authors`.`id` = 1), explain
+ assert_match %r(authors |.* const), explain
+ assert_match %(EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`author_id` = 1), explain
+ assert_match %r(posts |.* ALL), explain
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
index d18fb97e05..0719baaa23 100644
--- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb
@@ -25,25 +25,25 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
end
def test_columns_for_distinct_one_order
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@conn.columns_for_distinct("posts.id", ["posts.created_at desc"])
end
def test_columns_for_distinct_few_orders
- assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1",
+ assert_equal "posts.created_at AS alias_0, posts.position AS alias_1, posts.id",
@conn.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"])
end
def test_columns_for_distinct_with_case
assert_equal(
- "posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0",
+ "CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0, posts.id",
@conn.columns_for_distinct("posts.id",
["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"])
)
end
def test_columns_for_distinct_blank_not_nil_orders
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@conn.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "])
end
@@ -52,7 +52,7 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase
def order.to_sql
"posts.created_at desc"
end
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@conn.columns_for_distinct("posts.id", [order])
end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
index 62abd694bb..d7d9a2d732 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
@@ -36,7 +36,7 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase
assert connection.column_exists?(table_name, :key, :string)
end
ensure
- ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment
+ ActiveRecord::InternalMetadata[:environment] = connection.migration_context.current_environment
end
private
diff --git a/activerecord/test/cases/adapters/mysql2/table_options_test.rb b/activerecord/test/cases/adapters/mysql2/table_options_test.rb
index 9f6bd38a8c..1c92df940f 100644
--- a/activerecord/test/cases/adapters/mysql2/table_options_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/table_options_test.rb
@@ -42,3 +42,78 @@ class Mysql2TableOptionsTest < ActiveRecord::Mysql2TestCase
assert_match %r{COLLATE=utf8mb4_bin}, options
end
end
+
+class Mysql2DefaultEngineOptionSchemaDumpTest < ActiveRecord::Mysql2TestCase
+ include SchemaDumpingHelper
+ self.use_transactional_tests = false
+
+ def setup
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
+ end
+
+ def teardown
+ ActiveRecord::Base.connection.drop_table "mysql_table_options", if_exists: true
+ ActiveRecord::Migration.verbose = @verbose_was
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ end
+
+ test "schema dump includes ENGINE=InnoDB if not provided" do
+ ActiveRecord::Base.connection.create_table "mysql_table_options", force: true
+
+ output = dump_table_schema("mysql_table_options")
+ options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options]
+ assert_match %r{ENGINE=InnoDB}, options
+ end
+
+ test "schema dump includes ENGINE=InnoDB in legacy migrations" do
+ migration = Class.new(ActiveRecord::Migration[5.1]) do
+ def migrate(x)
+ create_table "mysql_table_options", force: true
+ end
+ end.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ output = dump_table_schema("mysql_table_options")
+ options = %r{create_table "mysql_table_options", options: "(?<options>.*)"}.match(output)[:options]
+ assert_match %r{ENGINE=InnoDB}, options
+ end
+end
+
+class Mysql2DefaultEngineOptionSqlOutputTest < ActiveRecord::Mysql2TestCase
+ self.use_transactional_tests = false
+
+ def setup
+ @logger_was = ActiveRecord::Base.logger
+ @log = StringIO.new
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new(@log)
+ ActiveRecord::Migration.verbose = false
+ end
+
+ def teardown
+ ActiveRecord::Base.logger = @logger_was
+ ActiveRecord::Migration.verbose = @verbose_was
+ ActiveRecord::Base.connection.drop_table "mysql_table_options", if_exists: true
+ ActiveRecord::SchemaMigration.delete_all rescue nil
+ end
+
+ test "new migrations do not contain default ENGINE=InnoDB option" do
+ ActiveRecord::Base.connection.create_table "mysql_table_options", force: true
+
+ assert_no_match %r{ENGINE=InnoDB}, @log.string
+ end
+
+ test "legacy migrations contain default ENGINE=InnoDB option" do
+ migration = Class.new(ActiveRecord::Migration[5.1]) do
+ def migrate(x)
+ create_table "mysql_table_options", force: true
+ end
+ end.new
+
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ assert_match %r{ENGINE=InnoDB}, @log.string
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb
index 25d9f69a89..52e283f247 100644
--- a/activerecord/test/cases/adapters/mysql2/transaction_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb
@@ -13,6 +13,7 @@ module ActiveRecord
setup do
@abort, Thread.abort_on_exception = Thread.abort_on_exception, false
+ Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception
@connection = ActiveRecord::Base.connection
@connection.clear_cache!
@@ -31,6 +32,7 @@ module ActiveRecord
@connection.drop_table "samples", if_exists: true
Thread.abort_on_exception = @abort
+ Thread.report_on_exception = @original_report_on_exception
end
test "raises Deadlocked when a deadlock is encountered" do
@@ -44,7 +46,7 @@ module ActiveRecord
Sample.transaction do
s1.lock!
barrier.wait
- s2.update_attributes value: 1
+ s2.update value: 1
end
end
@@ -52,7 +54,7 @@ module ActiveRecord
Sample.transaction do
s2.lock!
barrier.wait
- s1.update_attributes value: 2
+ s1.update value: 2
end
ensure
thread.join
@@ -60,9 +62,87 @@ module ActiveRecord
end
end
- test "raises TransactionTimeout when mysql raises ER_LOCK_WAIT_TIMEOUT" do
- assert_raises(ActiveRecord::TransactionTimeout) do
- ActiveRecord::Base.connection.execute("SIGNAL SQLSTATE 'HY000' SET MESSAGE_TEXT = 'Testing error', MYSQL_ERRNO = 1205;")
+ test "raises LockWaitTimeout when lock wait timeout exceeded" do
+ assert_raises(ActiveRecord::LockWaitTimeout) do
+ s = Sample.create!(value: 1)
+ latch1 = Concurrent::CountDownLatch.new
+ latch2 = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch1.count_down
+ latch2.wait
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch1.wait
+ Sample.connection.execute("SET innodb_lock_wait_timeout = 1")
+ Sample.lock.find(s.id)
+ end
+ ensure
+ Sample.connection.execute("SET innodb_lock_wait_timeout = DEFAULT")
+ latch2.count_down
+ thread.join
+ end
+ end
+ end
+
+ test "raises StatementTimeout when statement timeout exceeded" do
+ skip unless ActiveRecord::Base.connection.show_variable("max_execution_time")
+ assert_raises(ActiveRecord::StatementTimeout) do
+ s = Sample.create!(value: 1)
+ latch1 = Concurrent::CountDownLatch.new
+ latch2 = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch1.count_down
+ latch2.wait
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch1.wait
+ Sample.connection.execute("SET max_execution_time = 1")
+ Sample.lock.find(s.id)
+ end
+ ensure
+ Sample.connection.execute("SET max_execution_time = DEFAULT")
+ latch2.count_down
+ thread.join
+ end
+ end
+ end
+
+ test "raises QueryCanceled when canceling statement due to user request" do
+ assert_raises(ActiveRecord::QueryCanceled) do
+ s = Sample.create!(value: 1)
+ latch = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch.count_down
+ sleep(0.5)
+ conn = Sample.connection
+ pid = conn.query_value("SELECT id FROM information_schema.processlist WHERE info LIKE '% FOR UPDATE'")
+ conn.execute("KILL QUERY #{pid}")
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch.wait
+ Sample.lock.find(s.id)
+ end
+ ensure
+ thread.join
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
index b01f5d7f5a..97da96003d 100644
--- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb
@@ -54,7 +54,7 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase
end
@connection.columns("unsigned_types").select { |c| /^unsigned_/.match?(c.name) }.each do |column|
- assert column.unsigned?
+ assert_predicate column, :unsigned?
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 9929237546..308ad1d854 100644
--- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
@@ -59,6 +59,15 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase
assert_equal expected, add_index(:people, "lower(last_name)", using: type, unique: true)
end
+ expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name" bpchar_pattern_ops))
+ assert_equal expected, add_index(:people, :last_name, using: :gist, opclass: { last_name: :bpchar_pattern_ops })
+
+ expected = %(CREATE INDEX "index_people_on_last_name_and_first_name" ON "people" ("last_name" DESC NULLS LAST, "first_name" ASC))
+ assert_equal expected, add_index(:people, [:last_name, :first_name], order: { last_name: "DESC NULLS LAST", first_name: :asc })
+
+ expected = %(CREATE INDEX "index_people_on_last_name" ON "people" ("last_name" NULLS FIRST))
+ assert_equal expected, add_index(:people, :last_name, order: "NULLS FIRST")
+
assert_raise ArgumentError do
add_index(:people, :last_name, algorithm: :copy)
end
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index 0e9e86f425..58fa7532a2 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -39,12 +39,12 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
def test_column
assert_equal :string, @column.type
assert_equal "character varying(255)", @column.sql_type
- assert @column.array?
- assert_not @type.binary?
+ assert_predicate @column, :array?
+ assert_not_predicate @type, :binary?
ratings_column = PgArray.columns_hash["ratings"]
assert_equal :integer, ratings_column.type
- assert ratings_column.array?
+ assert_predicate ratings_column, :array?
end
def test_not_compatible_with_serialize_array
@@ -109,7 +109,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
assert_equal :text, column.type
assert_equal [], PgArray.column_defaults["snippets"]
- assert column.array?
+ assert_predicate column, :array?
end
def test_change_column_cant_make_non_array_column_to_array
@@ -228,7 +228,9 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
def test_insert_fixtures
tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"]
- @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays")
+ assert_deprecated do
+ @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays")
+ end
assert_equal(PgArray.last.tags, tag_values)
end
@@ -255,7 +257,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
x = PgArray.create!(tags: tags)
x.reload
- refute x.changed?
+ assert_not_predicate x, :changed?
end
def test_quoting_non_standard_delimiters
@@ -277,7 +279,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
x.reload
assert_equal %w(one two three), x.tags
- assert_not x.changed?
+ assert_not_predicate x, :changed?
end
def test_mutate_value_in_array
@@ -288,7 +290,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase
x.reload
assert_equal [{ "a" => "c" }, { "b" => "b" }], x.hstores
- assert_not x.changed?
+ assert_not_predicate x, :changed?
end
def test_datetime_with_timezone_awareness
diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
index df04299569..c8e728bbb6 100644
--- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb
@@ -29,20 +29,20 @@ class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlBitString.columns_hash["a_bit"]
assert_equal :bit, column.type
assert_equal "bit(8)", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlBitString.type_for_attribute("a_bit")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_bit_string_varying_column
column = PostgresqlBitString.columns_hash["a_bit_varying"]
assert_equal :bit_varying, column.type
assert_equal "bit varying(4)", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlBitString.type_for_attribute("a_bit_varying")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_default
diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
index a6bee113ff..64bb6906cd 100644
--- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -75,7 +75,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
def test_write_value
data = "\u001F"
record = ByteaDataType.create(payload: data)
- assert_not record.new_record?
+ assert_not_predicate record, :new_record?
assert_equal(data, record.payload)
end
@@ -101,14 +101,14 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase
data = File.read(File.join(__dir__, "..", "..", "..", "assets", "example.log"))
assert(data.size > 1)
record = ByteaDataType.create(payload: data)
- assert_not record.new_record?
+ assert_not_predicate record, :new_record?
assert_equal(data, record.payload)
assert_equal(data, ByteaDataType.where(id: record.id).first.payload)
end
def test_write_nil
record = ByteaDataType.create(payload: nil)
- assert_not record.new_record?
+ assert_not_predicate record, :new_record?
assert_nil(record.payload)
assert_nil(ByteaDataType.where(id: record.id).first.payload)
end
diff --git a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb
index adf461a9cc..6dba4f3e14 100644
--- a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb
@@ -33,7 +33,7 @@ module ActiveRecord
connection.change_column :strings, :somedate, :timestamp, array: true, cast_as: :timestamp
column = connection.columns(:strings).find { |c| c.name == "somedate" }
assert_equal :datetime, column.type
- assert column.array?
+ assert_predicate column, :array?
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb
index 050614cade..9eb0b7d99c 100644
--- a/activerecord/test/cases/adapters/postgresql/citext_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb
@@ -3,78 +3,76 @@
require "cases/helper"
require "support/schema_dumping_helper"
-if ActiveRecord::Base.connection.supports_extensions?
- class PostgresqlCitextTest < ActiveRecord::PostgreSQLTestCase
- include SchemaDumpingHelper
- class Citext < ActiveRecord::Base
- self.table_name = "citexts"
- end
-
- def setup
- @connection = ActiveRecord::Base.connection
+class PostgresqlCitextTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+ class Citext < ActiveRecord::Base
+ self.table_name = "citexts"
+ end
- enable_extension!("citext", @connection)
+ def setup
+ @connection = ActiveRecord::Base.connection
- @connection.create_table("citexts") do |t|
- t.citext "cival"
- end
- end
+ enable_extension!("citext", @connection)
- teardown do
- @connection.drop_table "citexts", if_exists: true
- disable_extension!("citext", @connection)
+ @connection.create_table("citexts") do |t|
+ t.citext "cival"
end
+ end
- def test_citext_enabled
- assert @connection.extension_enabled?("citext")
- end
+ teardown do
+ @connection.drop_table "citexts", if_exists: true
+ disable_extension!("citext", @connection)
+ end
- def test_column
- column = Citext.columns_hash["cival"]
- assert_equal :citext, column.type
- assert_equal "citext", column.sql_type
- assert_not column.array?
+ def test_citext_enabled
+ assert @connection.extension_enabled?("citext")
+ end
- type = Citext.type_for_attribute("cival")
- assert_not type.binary?
- end
+ def test_column
+ column = Citext.columns_hash["cival"]
+ assert_equal :citext, column.type
+ assert_equal "citext", column.sql_type
+ assert_not_predicate column, :array?
- def test_change_table_supports_json
- @connection.transaction do
- @connection.change_table("citexts") do |t|
- t.citext "username"
- end
- Citext.reset_column_information
- column = Citext.columns_hash["username"]
- assert_equal :citext, column.type
+ type = Citext.type_for_attribute("cival")
+ assert_not_predicate type, :binary?
+ end
- raise ActiveRecord::Rollback # reset the schema change
+ def test_change_table_supports_json
+ @connection.transaction do
+ @connection.change_table("citexts") do |t|
+ t.citext "username"
end
- ensure
Citext.reset_column_information
+ column = Citext.columns_hash["username"]
+ assert_equal :citext, column.type
+
+ raise ActiveRecord::Rollback # reset the schema change
end
+ ensure
+ Citext.reset_column_information
+ end
- def test_write
- x = Citext.new(cival: "Some CI Text")
- x.save!
- citext = Citext.first
- assert_equal "Some CI Text", citext.cival
+ def test_write
+ x = Citext.new(cival: "Some CI Text")
+ x.save!
+ citext = Citext.first
+ assert_equal "Some CI Text", citext.cival
- citext.cival = "Some NEW CI Text"
- citext.save!
+ citext.cival = "Some NEW CI Text"
+ citext.save!
- assert_equal "Some NEW CI Text", citext.reload.cival
- end
+ assert_equal "Some NEW CI Text", citext.reload.cival
+ end
- def test_select_case_insensitive
- @connection.execute "insert into citexts (cival) values('Cased Text')"
- x = Citext.where(cival: "cased text").first
- assert_equal "Cased Text", x.cival
- end
+ def test_select_case_insensitive
+ @connection.execute "insert into citexts (cival) values('Cased Text')"
+ x = Citext.where(cival: "cased text").first
+ assert_equal "Cased Text", x.cival
+ end
- def test_schema_dump_with_shorthand
- output = dump_table_schema("citexts")
- assert_match %r[t\.citext "cival"], output
- end
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("citexts")
+ assert_match %r[t\.citext "cival"], output
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb
index 5da95f7e2c..b0ce2694a3 100644
--- a/activerecord/test/cases/adapters/postgresql/composite_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb
@@ -51,10 +51,10 @@ class PostgresqlCompositeTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlComposite.columns_hash["address"]
assert_nil column.type
assert_equal "full_address", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlComposite.type_for_attribute("address")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_composite_mapping
@@ -113,10 +113,10 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlComposite.columns_hash["address"]
assert_equal :full_address, column.type
assert_equal "full_address", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlComposite.type_for_attribute("address")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_composite_mapping
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index 2bb217a8b1..d1b3c434e1 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -103,7 +103,7 @@ module ActiveRecord
end
def test_indexes_logs_name
- assert_deprecated { @connection.indexes("items", "hello") }
+ @connection.indexes("items")
assert_equal "SCHEMA", @subscriber.logged[0][1]
end
@@ -157,7 +157,7 @@ module ActiveRecord
original_connection_pid = @connection.query("select pg_backend_pid()")
# Sanity check.
- assert @connection.active?
+ assert_predicate @connection, :active?
if @connection.send(:postgresql_version) >= 90200
secondary_connection = ActiveRecord::Base.connection_pool.checkout
@@ -176,7 +176,7 @@ module ActiveRecord
@connection.verify!
- assert @connection.active?
+ assert_predicate @connection, :active?
# If we get no exception here, then either we re-connected successfully, or
# we never actually got disconnected.
diff --git a/activerecord/test/cases/adapters/postgresql/date_test.rb b/activerecord/test/cases/adapters/postgresql/date_test.rb
new file mode 100644
index 0000000000..a86abac2be
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/date_test.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/topic"
+
+class PostgresqlDateTest < ActiveRecord::PostgreSQLTestCase
+ def test_load_infinity_and_beyond
+ topic = Topic.find_by_sql("SELECT 'infinity'::date AS last_read").first
+ assert topic.last_read.infinite?, "timestamp should be infinite"
+ assert_operator topic.last_read, :>, 0
+
+ topic = Topic.find_by_sql("SELECT '-infinity'::date AS last_read").first
+ assert topic.last_read.infinite?, "timestamp should be infinite"
+ assert_operator topic.last_read, :<, 0
+ end
+
+ def test_save_infinity_and_beyond
+ topic = Topic.create!(last_read: 1.0 / 0.0)
+ assert_equal(1.0 / 0.0, topic.last_read)
+
+ topic = Topic.create!(last_read: -1.0 / 0.0)
+ assert_equal(-1.0 / 0.0, topic.last_read)
+ end
+
+ def test_bc_date
+ date = Date.new(0) - 1.week
+ topic = Topic.create!(last_read: date)
+ assert_equal date, Topic.find(topic.id).last_read
+ end
+
+ def test_bc_date_leap_year
+ date = Time.utc(-4, 2, 29).to_date
+ topic = Topic.create!(last_read: date)
+ assert_equal date, Topic.find(topic.id).last_read
+ end
+
+ def test_bc_date_year_zero
+ date = Time.utc(0, 4, 7).to_date
+ topic = Topic.create!(last_read: date)
+ assert_equal date, Topic.find(topic.id).last_read
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb
index 9c3817e2ad..eeaad94c27 100644
--- a/activerecord/test/cases/adapters/postgresql/domain_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb
@@ -30,10 +30,10 @@ class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlDomain.columns_hash["price"]
assert_equal :decimal, column.type
assert_equal "custom_money", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlDomain.type_for_attribute("price")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_domain_acts_like_basetype
@@ -44,6 +44,6 @@ class PostgresqlDomainTest < ActiveRecord::PostgreSQLTestCase
record.price = "34.15"
record.save!
- assert_equal BigDecimal.new("34.15"), record.reload.price
+ assert_equal BigDecimal("34.15"), record.reload.price
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb
index 3d3cbe11a3..6789ff63e7 100644
--- a/activerecord/test/cases/adapters/postgresql/enum_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb
@@ -32,10 +32,10 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlEnum.columns_hash["current_mood"]
assert_equal :enum, column.type
assert_equal "mood", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlEnum.type_for_attribute("current_mood")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_enum_defaults
@@ -73,7 +73,7 @@ class PostgresqlEnumTest < ActiveRecord::PostgreSQLTestCase
@connection.execute "INSERT INTO postgresql_enums VALUES (1, 'sad');"
stderr_output = capture(:stderr) { PostgresqlEnum.first }
- assert stderr_output.blank?
+ assert_predicate stderr_output, :blank?
end
def test_enum_type_cast
diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb
index 16fec94ede..be525383e9 100644
--- a/activerecord/test/cases/adapters/postgresql/explain_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb
@@ -1,22 +1,22 @@
# frozen_string_literal: true
require "cases/helper"
-require "models/developer"
-require "models/computer"
+require "models/author"
+require "models/post"
class PostgreSQLExplainTest < ActiveRecord::PostgreSQLTestCase
- fixtures :developers
+ fixtures :authors
def test_explain_for_one_query
- explain = Developer.where(id: 1).explain
- assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain
+ explain = Author.where(id: 1).explain
+ assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain
assert_match %(QUERY PLAN), explain
end
def test_explain_with_eager_loading
- explain = Developer.where(id: 1).includes(:audit_logs).explain
+ explain = Author.where(id: 1).includes(:posts).explain
assert_match %(QUERY PLAN), explain
- assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain
- assert_match %r(EXPLAIN for: SELECT "audit_logs"\.\* FROM "audit_logs" WHERE "audit_logs"\."developer_id" = (?:\$1 \[\["developer_id", 1\]\]|1)), explain
+ assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain
+ assert_match %r(EXPLAIN for: SELECT "posts"\.\* FROM "posts" WHERE "posts"\."author_id" = (?:\$1 \[\["author_id", 1\]\]|1)), explain
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
index e589e3ab1b..df97ab11e7 100644
--- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb
@@ -22,10 +22,6 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase
@connection = ActiveRecord::Base.connection
- unless @connection.supports_extensions?
- return skip("no extension support")
- end
-
@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
diff --git a/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb
new file mode 100644
index 0000000000..4fa315ad23
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/foreign_table_test.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/professor"
+
+if ActiveRecord::Base.connection.supports_foreign_tables?
+ class ForeignTableTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ class ForeignProfessor < ActiveRecord::Base
+ self.table_name = "foreign_professors"
+ end
+
+ class ForeignProfessorWithPk < ForeignProfessor
+ self.primary_key = "id"
+ end
+
+ def setup
+ @professor = Professor.create(name: "Nicola")
+
+ @connection = ActiveRecord::Base.connection
+ enable_extension!("postgres_fdw", @connection)
+
+ foreign_db_config = ARTest.connection_config["arunit2"]
+ @connection.execute <<-SQL
+ CREATE SERVER foreign_server
+ FOREIGN DATA WRAPPER postgres_fdw
+ OPTIONS (dbname '#{foreign_db_config["database"]}')
+ SQL
+
+ @connection.execute <<-SQL
+ CREATE USER MAPPING FOR CURRENT_USER
+ SERVER foreign_server
+ SQL
+
+ @connection.execute <<-SQL
+ CREATE FOREIGN TABLE foreign_professors (
+ id int,
+ name character varying NOT NULL
+ ) SERVER foreign_server OPTIONS (
+ table_name 'professors'
+ )
+ SQL
+ end
+
+ def teardown
+ disable_extension!("postgres_fdw", @connection)
+ @connection.execute <<-SQL
+ DROP SERVER IF EXISTS foreign_server CASCADE
+ SQL
+ end
+
+ def test_table_exists
+ table_name = ForeignProfessor.table_name
+ assert_not ActiveRecord::Base.connection.table_exists?(table_name)
+ end
+
+ def test_foreign_tables_are_valid_data_sources
+ table_name = ForeignProfessor.table_name
+ assert @connection.data_source_exists?(table_name), "'#{table_name}' should be a data source"
+ end
+
+ def test_foreign_tables
+ assert_equal ["foreign_professors"], @connection.foreign_tables
+ end
+
+ def test_foreign_table_exists
+ assert @connection.foreign_table_exists?("foreign_professors")
+ assert @connection.foreign_table_exists?(:foreign_professors)
+ assert_not @connection.foreign_table_exists?("nonexistingtable")
+ assert_not @connection.foreign_table_exists?("'")
+ assert_not @connection.foreign_table_exists?(nil)
+ end
+
+ def test_attribute_names
+ assert_equal ["id", "name"], ForeignProfessor.attribute_names
+ end
+
+ def test_attributes
+ professor = ForeignProfessorWithPk.find(@professor.id)
+ assert_equal @professor.attributes, professor.attributes
+ end
+
+ def test_does_not_have_a_primary_key
+ assert_nil ForeignProfessor.primary_key
+ end
+
+ def test_insert_record
+ # Explicit `id` here to avoid complex configurations to implicitly work with remote table
+ ForeignProfessorWithPk.create!(id: 100, name: "Leonardo")
+
+ professor = ForeignProfessorWithPk.last
+ assert_equal "Leonardo", professor.name
+ end
+
+ def test_update_record
+ professor = ForeignProfessorWithPk.find(@professor.id)
+ professor.name = "Albert"
+ professor.save!
+ professor.reload
+ assert_equal "Albert", professor.name
+ end
+
+ def test_delete_record
+ professor = ForeignProfessorWithPk.find(@professor.id)
+ assert_difference("ForeignProfessor.count", -1) { professor.destroy }
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
index c6f1e1727f..95dee3bf44 100644
--- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb
@@ -22,10 +22,10 @@ class PostgresqlFullTextTest < ActiveRecord::PostgreSQLTestCase
column = Tsvector.columns_hash["text_vector"]
assert_equal :tsvector, column.type
assert_equal "tsvector", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = Tsvector.type_for_attribute("text_vector")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_update_tsvector
diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
index e1ba00e07b..8c6f046553 100644
--- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb
@@ -39,10 +39,10 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlPoint.columns_hash["x"]
assert_equal :point, column.type
assert_equal "point", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlPoint.type_for_attribute("x")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_default
@@ -79,7 +79,7 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
p.reload
assert_equal ActiveRecord::Point.new(10.0, 25.0), p.x
- assert_not p.changed?
+ assert_not_predicate p, :changed?
end
def test_array_assignment
@@ -117,10 +117,10 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlPoint.columns_hash["legacy_x"]
assert_equal :point, column.type
assert_equal "point", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlPoint.type_for_attribute("legacy_x")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_legacy_default
@@ -157,7 +157,7 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase
p.reload
assert_equal [10.0, 25.0], p.legacy_x
- assert_not p.changed?
+ assert_not_predicate p, :changed?
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index 97a8a257c5..4b061a9375 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -3,378 +3,376 @@
require "cases/helper"
require "support/schema_dumping_helper"
-if ActiveRecord::Base.connection.supports_extensions?
- class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase
- include SchemaDumpingHelper
- class Hstore < ActiveRecord::Base
- self.table_name = "hstores"
+class PostgresqlHstoreTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+ class Hstore < ActiveRecord::Base
+ self.table_name = "hstores"
- store_accessor :settings, :language, :timezone
- end
+ store_accessor :settings, :language, :timezone
+ end
- class FakeParameters
- def to_unsafe_h
- { "hi" => "hi" }
- end
+ class FakeParameters
+ def to_unsafe_h
+ { "hi" => "hi" }
end
+ end
- def setup
- @connection = ActiveRecord::Base.connection
+ def setup
+ @connection = ActiveRecord::Base.connection
- enable_extension!("hstore", @connection)
+ enable_extension!("hstore", @connection)
- @connection.transaction do
- @connection.create_table("hstores") do |t|
- t.hstore "tags", default: ""
- t.hstore "payload", array: true
- t.hstore "settings"
- end
+ @connection.transaction do
+ @connection.create_table("hstores") do |t|
+ t.hstore "tags", default: ""
+ t.hstore "payload", array: true
+ t.hstore "settings"
end
- Hstore.reset_column_information
- @column = Hstore.columns_hash["tags"]
- @type = Hstore.type_for_attribute("tags")
- end
-
- teardown do
- @connection.drop_table "hstores", if_exists: true
- disable_extension!("hstore", @connection)
end
+ Hstore.reset_column_information
+ @column = Hstore.columns_hash["tags"]
+ @type = Hstore.type_for_attribute("tags")
+ end
- def test_hstore_included_in_extensions
- assert @connection.respond_to?(:extensions), "connection should have a list of extensions"
- assert_includes @connection.extensions, "hstore", "extension list should include hstore"
- end
+ teardown do
+ @connection.drop_table "hstores", if_exists: true
+ disable_extension!("hstore", @connection)
+ end
- def test_disable_enable_hstore
- assert @connection.extension_enabled?("hstore")
- @connection.disable_extension "hstore"
- assert_not @connection.extension_enabled?("hstore")
- @connection.enable_extension "hstore"
- assert @connection.extension_enabled?("hstore")
- ensure
- # Restore column(s) dropped by `drop extension hstore cascade;`
- load_schema
- end
+ def test_hstore_included_in_extensions
+ assert_respond_to @connection, :extensions
+ assert_includes @connection.extensions, "hstore", "extension list should include hstore"
+ end
- def test_column
- assert_equal :hstore, @column.type
- assert_equal "hstore", @column.sql_type
- assert_not @column.array?
+ def test_disable_enable_hstore
+ assert @connection.extension_enabled?("hstore")
+ @connection.disable_extension "hstore"
+ assert_not @connection.extension_enabled?("hstore")
+ @connection.enable_extension "hstore"
+ assert @connection.extension_enabled?("hstore")
+ ensure
+ # Restore column(s) dropped by `drop extension hstore cascade;`
+ load_schema
+ end
- assert_not @type.binary?
- end
+ def test_column
+ assert_equal :hstore, @column.type
+ assert_equal "hstore", @column.sql_type
+ assert_not_predicate @column, :array?
- def test_default
- @connection.add_column "hstores", "permissions", :hstore, default: '"users"=>"read", "articles"=>"write"'
- Hstore.reset_column_information
+ assert_not_predicate @type, :binary?
+ end
- assert_equal({ "users" => "read", "articles" => "write" }, Hstore.column_defaults["permissions"])
- assert_equal({ "users" => "read", "articles" => "write" }, Hstore.new.permissions)
- ensure
- Hstore.reset_column_information
- end
+ def test_default
+ @connection.add_column "hstores", "permissions", :hstore, default: '"users"=>"read", "articles"=>"write"'
+ Hstore.reset_column_information
- def test_change_table_supports_hstore
- @connection.transaction do
- @connection.change_table("hstores") do |t|
- t.hstore "users", default: ""
- end
- Hstore.reset_column_information
- column = Hstore.columns_hash["users"]
- assert_equal :hstore, column.type
+ assert_equal({ "users" => "read", "articles" => "write" }, Hstore.column_defaults["permissions"])
+ assert_equal({ "users" => "read", "articles" => "write" }, Hstore.new.permissions)
+ ensure
+ Hstore.reset_column_information
+ end
- raise ActiveRecord::Rollback # reset the schema change
+ def test_change_table_supports_hstore
+ @connection.transaction do
+ @connection.change_table("hstores") do |t|
+ t.hstore "users", default: ""
end
- ensure
Hstore.reset_column_information
+ column = Hstore.columns_hash["users"]
+ assert_equal :hstore, column.type
+
+ raise ActiveRecord::Rollback # reset the schema change
end
+ ensure
+ Hstore.reset_column_information
+ end
- def test_hstore_migration
- hstore_migration = Class.new(ActiveRecord::Migration::Current) do
- def change
- change_table("hstores") do |t|
- t.hstore :keys
- end
+ def test_hstore_migration
+ hstore_migration = Class.new(ActiveRecord::Migration::Current) do
+ def change
+ change_table("hstores") do |t|
+ t.hstore :keys
end
end
-
- hstore_migration.new.suppress_messages do
- hstore_migration.migrate(:up)
- assert_includes @connection.columns(:hstores).map(&:name), "keys"
- hstore_migration.migrate(:down)
- assert_not_includes @connection.columns(:hstores).map(&:name), "keys"
- end
end
- def test_cast_value_on_write
- x = Hstore.new tags: { "bool" => true, "number" => 5 }
- assert_equal({ "bool" => true, "number" => 5 }, x.tags_before_type_cast)
- assert_equal({ "bool" => "true", "number" => "5" }, x.tags)
- x.save
- assert_equal({ "bool" => "true", "number" => "5" }, x.reload.tags)
+ hstore_migration.new.suppress_messages do
+ hstore_migration.migrate(:up)
+ assert_includes @connection.columns(:hstores).map(&:name), "keys"
+ hstore_migration.migrate(:down)
+ assert_not_includes @connection.columns(:hstores).map(&:name), "keys"
end
+ end
- def test_type_cast_hstore
- assert_equal({ "1" => "2" }, @type.deserialize("\"1\"=>\"2\""))
- assert_equal({}, @type.deserialize(""))
- assert_equal({ "key" => nil }, @type.deserialize("key => NULL"))
- assert_equal({ "c" => "}", '"a"' => 'b "a b' }, @type.deserialize(%q(c=>"}", "\"a\""=>"b \"a b")))
- end
+ def test_cast_value_on_write
+ x = Hstore.new tags: { "bool" => true, "number" => 5 }
+ assert_equal({ "bool" => true, "number" => 5 }, x.tags_before_type_cast)
+ assert_equal({ "bool" => "true", "number" => "5" }, x.tags)
+ x.save
+ assert_equal({ "bool" => "true", "number" => "5" }, x.reload.tags)
+ end
- def test_with_store_accessors
- x = Hstore.new(language: "fr", timezone: "GMT")
- assert_equal "fr", x.language
- assert_equal "GMT", x.timezone
+ def test_type_cast_hstore
+ assert_equal({ "1" => "2" }, @type.deserialize("\"1\"=>\"2\""))
+ assert_equal({}, @type.deserialize(""))
+ assert_equal({ "key" => nil }, @type.deserialize("key => NULL"))
+ assert_equal({ "c" => "}", '"a"' => 'b "a b' }, @type.deserialize(%q(c=>"}", "\"a\""=>"b \"a b")))
+ end
- x.save!
- x = Hstore.first
- assert_equal "fr", x.language
- assert_equal "GMT", x.timezone
+ def test_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- x.language = "de"
- x.save!
+ x.save!
+ x = Hstore.first
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- x = Hstore.first
- assert_equal "de", x.language
- assert_equal "GMT", x.timezone
- end
+ x.language = "de"
+ x.save!
- def test_duplication_with_store_accessors
- x = Hstore.new(language: "fr", timezone: "GMT")
- assert_equal "fr", x.language
- assert_equal "GMT", x.timezone
+ x = Hstore.first
+ assert_equal "de", x.language
+ assert_equal "GMT", x.timezone
+ end
- y = x.dup
- assert_equal "fr", y.language
- assert_equal "GMT", y.timezone
- end
+ def test_duplication_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- def test_yaml_round_trip_with_store_accessors
- x = Hstore.new(language: "fr", timezone: "GMT")
- assert_equal "fr", x.language
- assert_equal "GMT", x.timezone
+ y = x.dup
+ assert_equal "fr", y.language
+ assert_equal "GMT", y.timezone
+ end
- y = YAML.load(YAML.dump(x))
- assert_equal "fr", y.language
- assert_equal "GMT", y.timezone
- end
+ def test_yaml_round_trip_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
- def test_changes_in_place
- hstore = Hstore.create!(settings: { "one" => "two" })
- hstore.settings["three"] = "four"
- hstore.save!
- hstore.reload
+ y = YAML.load(YAML.dump(x))
+ assert_equal "fr", y.language
+ assert_equal "GMT", y.timezone
+ end
- assert_equal "four", hstore.settings["three"]
- assert_not hstore.changed?
- end
+ def test_changes_in_place
+ hstore = Hstore.create!(settings: { "one" => "two" })
+ hstore.settings["three"] = "four"
+ hstore.save!
+ hstore.reload
- def test_dirty_from_user_equal
- settings = { "alongkey" => "anything", "key" => "value" }
- hstore = Hstore.create!(settings: settings)
+ assert_equal "four", hstore.settings["three"]
+ assert_not_predicate hstore, :changed?
+ end
- hstore.settings = { "key" => "value", "alongkey" => "anything" }
- assert_equal settings, hstore.settings
- refute hstore.changed?
- end
+ def test_dirty_from_user_equal
+ settings = { "alongkey" => "anything", "key" => "value" }
+ hstore = Hstore.create!(settings: settings)
- def test_hstore_dirty_from_database_equal
- settings = { "alongkey" => "anything", "key" => "value" }
- hstore = Hstore.create!(settings: settings)
- hstore.reload
+ hstore.settings = { "key" => "value", "alongkey" => "anything" }
+ assert_equal settings, hstore.settings
+ assert_not_predicate hstore, :changed?
+ end
- assert_equal settings, hstore.settings
- hstore.settings = settings
- refute hstore.changed?
- end
+ def test_hstore_dirty_from_database_equal
+ settings = { "alongkey" => "anything", "key" => "value" }
+ hstore = Hstore.create!(settings: settings)
+ hstore.reload
- def test_gen1
- assert_equal('" "=>""', @type.serialize(" " => ""))
- end
+ assert_equal settings, hstore.settings
+ hstore.settings = settings
+ assert_not_predicate hstore, :changed?
+ end
- def test_gen2
- assert_equal('","=>""', @type.serialize("," => ""))
- end
+ def test_gen1
+ assert_equal('" "=>""', @type.serialize(" " => ""))
+ end
- def test_gen3
- assert_equal('"="=>""', @type.serialize("=" => ""))
- end
+ def test_gen2
+ assert_equal('","=>""', @type.serialize("," => ""))
+ end
- def test_gen4
- assert_equal('">"=>""', @type.serialize(">" => ""))
- end
+ def test_gen3
+ assert_equal('"="=>""', @type.serialize("=" => ""))
+ end
- def test_parse1
- assert_equal({ "a" => nil, "b" => nil, "c" => "NuLl", "null" => "c" }, @type.deserialize('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
- end
+ def test_gen4
+ assert_equal('">"=>""', @type.serialize(">" => ""))
+ end
- def test_parse2
- assert_equal({ " " => " " }, @type.deserialize("\\ =>\\ "))
- end
+ def test_parse1
+ assert_equal({ "a" => nil, "b" => nil, "c" => "NuLl", "null" => "c" }, @type.deserialize('a=>null,b=>NuLl,c=>"NuLl",null=>c'))
+ end
- def test_parse3
- assert_equal({ "=" => ">" }, @type.deserialize("==>>"))
- end
+ def test_parse2
+ assert_equal({ " " => " " }, @type.deserialize("\\ =>\\ "))
+ end
- def test_parse4
- assert_equal({ "=a" => "q=w" }, @type.deserialize('\=a=>q=w'))
- end
+ def test_parse3
+ assert_equal({ "=" => ">" }, @type.deserialize("==>>"))
+ end
- def test_parse5
- assert_equal({ "=a" => "q=w" }, @type.deserialize('"=a"=>q\=w'))
- end
+ def test_parse4
+ assert_equal({ "=a" => "q=w" }, @type.deserialize('\=a=>q=w'))
+ end
- def test_parse6
- assert_equal({ "\"a" => "q>w" }, @type.deserialize('"\"a"=>q>w'))
- end
+ def test_parse5
+ assert_equal({ "=a" => "q=w" }, @type.deserialize('"=a"=>q\=w'))
+ end
- def test_parse7
- assert_equal({ "\"a" => "q\"w" }, @type.deserialize('\"a=>q"w'))
- end
+ def test_parse6
+ assert_equal({ "\"a" => "q>w" }, @type.deserialize('"\"a"=>q>w'))
+ end
- def test_rewrite
- @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
- x = Hstore.first
- x.tags = { '"a\'' => "b" }
- assert x.save!
- end
+ def test_parse7
+ assert_equal({ "\"a" => "q\"w" }, @type.deserialize('\"a=>q"w'))
+ end
- def test_select
- @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
- x = Hstore.first
- assert_equal({ "1" => "2" }, x.tags)
- end
+ def test_rewrite
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
+ x = Hstore.first
+ x.tags = { '"a\'' => "b" }
+ assert x.save!
+ end
- def test_array_cycle
- assert_array_cycle([{ "AA" => "BB", "CC" => "DD" }, { "AA" => nil }])
- end
+ def test_select
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2')"
+ x = Hstore.first
+ assert_equal({ "1" => "2" }, x.tags)
+ end
- def test_array_strings_with_quotes
- assert_array_cycle([{ "this has" => 'some "s that need to be escaped"' }])
- end
+ def test_array_cycle
+ assert_array_cycle([{ "AA" => "BB", "CC" => "DD" }, { "AA" => nil }])
+ end
- def test_array_strings_with_commas
- assert_array_cycle([{ "this,has" => "many,values" }])
- end
+ def test_array_strings_with_quotes
+ assert_array_cycle([{ "this has" => 'some "s that need to be escaped"' }])
+ end
- def test_array_strings_with_array_delimiters
- assert_array_cycle(["{" => "}"])
- end
+ def test_array_strings_with_commas
+ assert_array_cycle([{ "this,has" => "many,values" }])
+ end
- def test_array_strings_with_null_strings
- assert_array_cycle([{ "NULL" => "NULL" }])
- end
+ def test_array_strings_with_array_delimiters
+ assert_array_cycle(["{" => "}"])
+ end
- def test_contains_nils
- assert_array_cycle([{ "NULL" => nil }])
- end
+ def test_array_strings_with_null_strings
+ assert_array_cycle([{ "NULL" => "NULL" }])
+ end
- def test_select_multikey
- @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')"
- x = Hstore.first
- assert_equal({ "1" => "2", "2" => "3" }, x.tags)
- end
+ def test_contains_nils
+ assert_array_cycle([{ "NULL" => nil }])
+ end
- def test_create
- assert_cycle("a" => "b", "1" => "2")
- end
+ def test_select_multikey
+ @connection.execute "insert into hstores (tags) VALUES ('1=>2,2=>3')"
+ x = Hstore.first
+ assert_equal({ "1" => "2", "2" => "3" }, x.tags)
+ end
- def test_nil
- assert_cycle("a" => nil)
- end
+ def test_create
+ assert_cycle("a" => "b", "1" => "2")
+ end
- def test_quotes
- assert_cycle("a" => 'b"ar', '1"foo' => "2")
- end
+ def test_nil
+ assert_cycle("a" => nil)
+ end
- def test_whitespace
- assert_cycle("a b" => "b ar", '1"foo' => "2")
- end
+ def test_quotes
+ assert_cycle("a" => 'b"ar', '1"foo' => "2")
+ end
- def test_backslash
- assert_cycle('a\\b' => 'b\\ar', '1"foo' => "2")
- end
+ def test_whitespace
+ assert_cycle("a b" => "b ar", '1"foo' => "2")
+ end
- def test_comma
- assert_cycle("a, b" => "bar", '1"foo' => "2")
- end
+ def test_backslash
+ assert_cycle('a\\b' => 'b\\ar', '1"foo' => "2")
+ end
- def test_arrow
- assert_cycle("a=>b" => "bar", '1"foo' => "2")
- end
+ def test_comma
+ assert_cycle("a, b" => "bar", '1"foo' => "2")
+ end
- def test_quoting_special_characters
- assert_cycle("ca" => "cà", "ac" => "àc")
- end
+ def test_arrow
+ assert_cycle("a=>b" => "bar", '1"foo' => "2")
+ end
- def test_multiline
- assert_cycle("a\nb" => "c\nd")
- end
+ def test_quoting_special_characters
+ assert_cycle("ca" => "cà", "ac" => "àc")
+ end
- class TagCollection
- def initialize(hash); @hash = hash end
- def to_hash; @hash end
- def self.load(hash); new(hash) end
- def self.dump(object); object.to_hash end
- end
+ def test_multiline
+ assert_cycle("a\nb" => "c\nd")
+ end
- class HstoreWithSerialize < Hstore
- serialize :tags, TagCollection
- end
+ class TagCollection
+ def initialize(hash); @hash = hash end
+ def to_hash; @hash end
+ def self.load(hash); new(hash) end
+ def self.dump(object); object.to_hash end
+ end
- def test_hstore_with_serialized_attributes
- HstoreWithSerialize.create! tags: TagCollection.new("one" => "two")
- record = HstoreWithSerialize.first
- assert_instance_of TagCollection, record.tags
- assert_equal({ "one" => "two" }, record.tags.to_hash)
- record.tags = TagCollection.new("three" => "four")
- record.save!
- assert_equal({ "three" => "four" }, HstoreWithSerialize.first.tags.to_hash)
- end
+ class HstoreWithSerialize < Hstore
+ serialize :tags, TagCollection
+ end
- def test_clone_hstore_with_serialized_attributes
- HstoreWithSerialize.create! tags: TagCollection.new("one" => "two")
- record = HstoreWithSerialize.first
- dupe = record.dup
- assert_equal({ "one" => "two" }, dupe.tags.to_hash)
- end
+ def test_hstore_with_serialized_attributes
+ HstoreWithSerialize.create! tags: TagCollection.new("one" => "two")
+ record = HstoreWithSerialize.first
+ assert_instance_of TagCollection, record.tags
+ assert_equal({ "one" => "two" }, record.tags.to_hash)
+ record.tags = TagCollection.new("three" => "four")
+ record.save!
+ assert_equal({ "three" => "four" }, HstoreWithSerialize.first.tags.to_hash)
+ end
- def test_schema_dump_with_shorthand
- output = dump_table_schema("hstores")
- assert_match %r[t\.hstore "tags",\s+default: {}], output
- end
+ def test_clone_hstore_with_serialized_attributes
+ HstoreWithSerialize.create! tags: TagCollection.new("one" => "two")
+ record = HstoreWithSerialize.first
+ dupe = record.dup
+ assert_equal({ "one" => "two" }, dupe.tags.to_hash)
+ end
+
+ def test_schema_dump_with_shorthand
+ output = dump_table_schema("hstores")
+ assert_match %r[t\.hstore "tags",\s+default: {}], output
+ end
+
+ def test_supports_to_unsafe_h_values
+ assert_equal("\"hi\"=>\"hi\"", @type.serialize(FakeParameters.new))
+ end
- def test_supports_to_unsafe_h_values
- assert_equal("\"hi\"=>\"hi\"", @type.serialize(FakeParameters.new))
+ private
+ def assert_array_cycle(array)
+ # test creation
+ x = Hstore.create!(payload: array)
+ x.reload
+ assert_equal(array, x.payload)
+
+ # test updating
+ x = Hstore.create!(payload: [])
+ x.payload = array
+ x.save!
+ x.reload
+ assert_equal(array, x.payload)
end
- private
- def assert_array_cycle(array)
- # test creation
- x = Hstore.create!(payload: array)
- x.reload
- assert_equal(array, x.payload)
-
- # test updating
- x = Hstore.create!(payload: [])
- x.payload = array
- x.save!
- x.reload
- assert_equal(array, x.payload)
- end
+ def assert_cycle(hash)
+ # test creation
+ x = Hstore.create!(tags: hash)
+ x.reload
+ assert_equal(hash, x.tags)
- def assert_cycle(hash)
- # test creation
- x = Hstore.create!(tags: hash)
- x.reload
- assert_equal(hash, x.tags)
-
- # test updating
- x = Hstore.create!(tags: {})
- x.tags = hash
- x.save!
- x.reload
- assert_equal(hash, x.tags)
- end
- end
+ # test updating
+ x = Hstore.create!(tags: {})
+ x.tags = hash
+ x.save!
+ x.reload
+ assert_equal(hash, x.tags)
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
index 0b18c0c9d7..5e56ce8427 100644
--- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb
@@ -13,6 +13,7 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
@connection.create_table(:postgresql_infinities) do |t|
t.float :float
t.datetime :datetime
+ t.date :date
end
end
@@ -43,11 +44,25 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
end
test "type casting infinity on a datetime column" do
+ record = PostgresqlInfinity.create!(datetime: "infinity")
+ record.reload
+ assert_equal Float::INFINITY, record.datetime
+
record = PostgresqlInfinity.create!(datetime: Float::INFINITY)
record.reload
assert_equal Float::INFINITY, record.datetime
end
+ test "type casting infinity on a date column" do
+ record = PostgresqlInfinity.create!(date: "infinity")
+ record.reload
+ assert_equal Float::INFINITY, record.date
+
+ record = PostgresqlInfinity.create!(date: Float::INFINITY)
+ record.reload
+ assert_equal Float::INFINITY, record.date
+ end
+
test "update_all with infinity on a datetime column" do
record = PostgresqlInfinity.create!
PostgresqlInfinity.update_all(datetime: Float::INFINITY)
@@ -68,4 +83,28 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase
PostgresqlInfinity.reset_column_information
end
end
+
+ test "where clause with infinite range on a datetime column" do
+ record = PostgresqlInfinity.create!(datetime: Time.current)
+
+ string = PostgresqlInfinity.where(datetime: "-infinity".."infinity")
+ assert_equal record, string.take
+
+ infinity = PostgresqlInfinity.where(datetime: -::Float::INFINITY..::Float::INFINITY)
+ assert_equal record, infinity.take
+
+ assert_equal infinity.to_sql, string.to_sql
+ end
+
+ test "where clause with infinite range on a date column" do
+ record = PostgresqlInfinity.create!(date: Date.current)
+
+ string = PostgresqlInfinity.where(date: "-infinity".."infinity")
+ assert_equal record, string.take
+
+ infinity = PostgresqlInfinity.where(date: -::Float::INFINITY..::Float::INFINITY)
+ assert_equal record, infinity.take
+
+ assert_equal infinity.to_sql, string.to_sql
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
index eca29f2892..8349ee6ee2 100644
--- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
@@ -31,10 +31,10 @@ class PostgresqlLtreeTest < ActiveRecord::PostgreSQLTestCase
column = Ltree.columns_hash["path"]
assert_equal :ltree, column.type
assert_equal "ltree", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = Ltree.type_for_attribute("path")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_write
diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb
index 563f0bbfae..be3590e8dd 100644
--- a/activerecord/test/cases/adapters/postgresql/money_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/money_test.rb
@@ -26,15 +26,15 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase
assert_equal :money, column.type
assert_equal "money", column.sql_type
assert_equal 2, column.scale
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlMoney.type_for_attribute("wealth")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_default
- assert_equal BigDecimal.new("150.55"), PostgresqlMoney.column_defaults["depth"]
- assert_equal BigDecimal.new("150.55"), PostgresqlMoney.new.depth
+ assert_equal BigDecimal("150.55"), PostgresqlMoney.column_defaults["depth"]
+ assert_equal BigDecimal("150.55"), PostgresqlMoney.new.depth
end
def test_money_values
@@ -65,7 +65,7 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase
money = PostgresqlMoney.create(wealth: "987.65".dup)
assert_equal 987.65, money.wealth
- new_value = BigDecimal.new("123.45")
+ new_value = BigDecimal("123.45")
money.wealth = new_value
money.save!
money.reload
diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb
index f461544a85..736570451b 100644
--- a/activerecord/test/cases/adapters/postgresql/network_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/network_test.rb
@@ -24,30 +24,30 @@ class PostgresqlNetworkTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlNetworkAddress.columns_hash["cidr_address"]
assert_equal :cidr, column.type
assert_equal "cidr", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlNetworkAddress.type_for_attribute("cidr_address")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_inet_column
column = PostgresqlNetworkAddress.columns_hash["inet_address"]
assert_equal :inet, column.type
assert_equal "inet", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlNetworkAddress.type_for_attribute("inet_address")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_macaddr_column
column = PostgresqlNetworkAddress.columns_hash["mac_address"]
assert_equal :macaddr, column.type
assert_equal "macaddr", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = PostgresqlNetworkAddress.type_for_attribute("mac_address")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_network_types
diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
index f199519d86..cbb6cd42b5 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -248,12 +248,12 @@ module ActiveRecord
def test_index_with_opclass
with_example_table do
- @connection.add_index "ex", "data varchar_pattern_ops"
- index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" }
- assert_equal "data varchar_pattern_ops", index.columns
+ @connection.add_index "ex", "data", opclass: "varchar_pattern_ops"
+ index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" }
+ assert_equal ["data"], index.columns
- @connection.remove_index "ex", "data varchar_pattern_ops"
- assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" }
+ @connection.remove_index "ex", "data"
+ assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data" }
end
end
@@ -263,25 +263,25 @@ module ActiveRecord
end
def test_columns_for_distinct_one_order
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc"])
end
def test_columns_for_distinct_few_orders
- assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1",
+ assert_equal "posts.created_at AS alias_0, posts.position AS alias_1, posts.id",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"])
end
def test_columns_for_distinct_with_case
assert_equal(
- "posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0",
+ "CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0, posts.id",
@connection.columns_for_distinct("posts.id",
["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"])
)
end
def test_columns_for_distinct_blank_not_nil_orders
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@connection.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "])
end
@@ -290,23 +290,23 @@ module ActiveRecord
def order.to_sql
"posts.created_at desc"
end
- assert_equal "posts.id, posts.created_at AS alias_0",
+ assert_equal "posts.created_at AS alias_0, posts.id",
@connection.columns_for_distinct("posts.id", [order])
end
def test_columns_for_distinct_with_nulls
- assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"])
- assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"])
+ assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"])
+ assert_equal "posts.updater_id AS alias_0, posts.title", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"])
end
def test_columns_for_distinct_without_order_specifiers
- assert_equal "posts.title, posts.updater_id AS alias_0",
+ assert_equal "posts.updater_id AS alias_0, posts.title",
@connection.columns_for_distinct("posts.title", ["posts.updater_id"])
- assert_equal "posts.title, posts.updater_id AS alias_0",
+ assert_equal "posts.updater_id AS alias_0, posts.title",
@connection.columns_for_distinct("posts.title", ["posts.updater_id nulls last"])
- assert_equal "posts.title, posts.updater_id AS alias_0",
+ assert_equal "posts.updater_id AS alias_0, posts.title",
@connection.columns_for_distinct("posts.title", ["posts.updater_id nulls first"])
end
diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb
index b4a776d04d..261c24634e 100644
--- a/activerecord/test/cases/adapters/postgresql/range_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/range_test.rb
@@ -134,10 +134,10 @@ _SQL
end
def test_numrange_values
- assert_equal BigDecimal.new("0.1")..BigDecimal.new("0.2"), @first_range.num_range
- assert_equal BigDecimal.new("0.1")...BigDecimal.new("0.2"), @second_range.num_range
- assert_equal BigDecimal.new("0.1")...BigDecimal.new("Infinity"), @third_range.num_range
- assert_equal BigDecimal.new("-Infinity")...BigDecimal.new("Infinity"), @fourth_range.num_range
+ 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
@@ -232,16 +232,67 @@ _SQL
end
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_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_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_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_timezone_awareness_tsrange_preserve_usec
+ tz = "Pacific Time (US & Canada)"
+
+ 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
+
+ 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
+
+ record.save!
+ record.reload
+
+ 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_numrange
assert_equal_round_trip(@new_range, :num_range,
- BigDecimal.new("0.5")...BigDecimal.new("1"))
+ BigDecimal("0.5")...BigDecimal("1"))
end
def test_update_numrange
assert_equal_round_trip(@first_range, :num_range,
- BigDecimal.new("0.5")...BigDecimal.new("1"))
+ BigDecimal("0.5")...BigDecimal("1"))
assert_nil_round_trip(@first_range, :num_range,
- BigDecimal.new("0.5")...BigDecimal.new("0.5"))
+ BigDecimal("0.5")...BigDecimal("0.5"))
end
def test_create_daterange
@@ -307,6 +358,18 @@ _SQL
end
end
+ def test_infinity_values
+ PostgresqlRange.create!(int4_range: 1..Float::INFINITY,
+ int8_range: -Float::INFINITY..0,
+ float_range: -Float::INFINITY..Float::INFINITY)
+
+ record = PostgresqlRange.first
+
+ assert_equal(1...Float::INFINITY, record.int4_range)
+ assert_equal(-Float::INFINITY...1, record.int8_range)
+ assert_equal(-Float::INFINITY...Float::INFINITY, record.float_range)
+ end
+
private
def assert_equal_round_trip(range, attribute, value)
round_trip(range, attribute, value)
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index 5a64da028b..7ad03c194f 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -459,7 +459,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase
assert_equal :btree, index_d.using
assert_equal :gin, index_e.using
- assert_equal :desc, index_d.orders[INDEX_D_COLUMN]
+ assert_equal :desc, index_d.orders
end
end
@@ -500,6 +500,66 @@ class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase
end
end
+class SchemaIndexOpclassTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "trains" do |t|
+ t.string :name
+ t.text :description
+ end
+ end
+
+ teardown do
+ @connection.drop_table "trains", if_exists: true
+ end
+
+ def test_string_opclass_is_dumped
+ @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name text_pattern_ops, description text_pattern_ops)"
+
+ output = dump_table_schema "trains"
+
+ assert_match(/opclass: :text_pattern_ops/, output)
+ end
+
+ def test_non_default_opclass_is_dumped
+ @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name, description text_pattern_ops)"
+
+ output = dump_table_schema "trains"
+
+ assert_match(/opclass: \{ description: :text_pattern_ops \}/, output)
+ end
+end
+
+class SchemaIndexNullsOrderTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table "trains" do |t|
+ t.string :name
+ t.text :description
+ end
+ end
+
+ teardown do
+ @connection.drop_table "trains", if_exists: true
+ end
+
+ def test_nulls_order_is_dumped
+ @connection.execute "CREATE INDEX trains_name_and_description ON trains USING btree(name NULLS FIRST, description)"
+ output = dump_table_schema "trains"
+ assert_match(/order: \{ name: "NULLS FIRST" \}/, output)
+ end
+
+ def test_non_default_order_with_nulls_is_dumped
+ @connection.execute "CREATE INDEX trains_name_and_desc ON trains USING btree(name DESC NULLS LAST, description)"
+ output = dump_table_schema "trains"
+ assert_match(/order: \{ name: "DESC NULLS LAST" \}/, output)
+ end
+end
+
class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCase
setup do
@connection = ActiveRecord::Base.connection
@@ -534,7 +594,7 @@ class DefaultsUsingMultipleSchemasAndDomainTest < ActiveRecord::PostgreSQLTestCa
end
def test_decimal_defaults_in_new_schema_when_overriding_domain
- assert_equal BigDecimal.new("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed"
+ assert_equal BigDecimal("3.14159265358979323846"), Default.new.decimal_col, "Default of decimal column was not correctly parsed"
end
def test_bpchar_defaults_in_new_schema_when_overriding_domain
diff --git a/activerecord/test/cases/adapters/postgresql/serial_test.rb b/activerecord/test/cases/adapters/postgresql/serial_test.rb
index df7875dbf2..83ea86be6d 100644
--- a/activerecord/test/cases/adapters/postgresql/serial_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/serial_test.rb
@@ -24,14 +24,14 @@ class PostgresqlSerialTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlSerial.columns_hash["seq"]
assert_equal :integer, column.type
assert_equal "integer", column.sql_type
- assert column.serial?
+ assert_predicate column, :serial?
end
def test_not_serial_column
column = PostgresqlSerial.columns_hash["serials_id"]
assert_equal :integer, column.type
assert_equal "integer", column.sql_type
- assert_not column.serial?
+ assert_not_predicate column, :serial?
end
def test_schema_dump_with_shorthand
@@ -66,14 +66,14 @@ class PostgresqlBigSerialTest < ActiveRecord::PostgreSQLTestCase
column = PostgresqlBigSerial.columns_hash["seq"]
assert_equal :integer, column.type
assert_equal "bigint", column.sql_type
- assert column.serial?
+ assert_predicate column, :serial?
end
def test_not_bigserial_column
column = PostgresqlBigSerial.columns_hash["serials_id"]
assert_equal :integer, column.type
assert_equal "bigint", column.sql_type
- assert_not column.serial?
+ assert_not_predicate column, :serial?
end
def test_schema_dump_with_shorthand
@@ -111,7 +111,7 @@ module SequenceNameDetectionTestCases
columns = @connection.columns(:foo)
columns.each do |column|
assert_equal :integer, column.type
- assert column.serial?
+ assert_predicate column, :serial?
end
end
@@ -121,4 +121,36 @@ module SequenceNameDetectionTestCases
assert_match %r{t\.bigserial\s+"bar_baz_id",\s+null: false$}, output
end
end
+
+ class LongerSequenceNameDetectionTest < ActiveRecord::PostgreSQLTestCase
+ include SchemaDumpingHelper
+
+ def setup
+ @table_name = "long_table_name_to_test_sequence_name_detection_for_serial_cols"
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table @table_name, force: true do |t|
+ t.serial :seq
+ t.bigserial :bigseq
+ end
+ end
+
+ def teardown
+ @connection.drop_table @table_name, if_exists: true
+ end
+
+ def test_serial_columns
+ columns = @connection.columns(@table_name)
+ columns.each do |column|
+ assert_equal :integer, column.type
+ assert_predicate column, :serial?
+ end
+ end
+
+ def test_schema_dump_with_long_table_name
+ output = dump_table_schema @table_name
+ assert_match %r{create_table "#{@table_name}", force: :cascade}, output
+ assert_match %r{t\.serial\s+"seq",\s+null: false$}, output
+ assert_match %r{t\.bigserial\s+"bigseq",\s+null: false$}, output
+ end
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
index a3eb4f9e67..fef4b02b04 100644
--- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
@@ -23,7 +23,7 @@ module ActiveRecord
assert_equal "bar", cache["foo"]
pid = fork {
- lookup = cache["foo"];
+ lookup = cache["foo"]
exit!(!lookup)
}
diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb
index f56adf4a5e..984b2f5ea4 100644
--- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb
@@ -14,6 +14,7 @@ module ActiveRecord
setup do
@abort, Thread.abort_on_exception = Thread.abort_on_exception, false
+ Thread.report_on_exception, @original_report_on_exception = false, Thread.report_on_exception
@connection = ActiveRecord::Base.connection
@@ -31,6 +32,7 @@ module ActiveRecord
@connection.drop_table "samples", if_exists: true
Thread.abort_on_exception = @abort
+ Thread.report_on_exception = @original_report_on_exception
end
test "raises SerializationFailure when a serialization failure occurs" do
@@ -74,7 +76,7 @@ module ActiveRecord
Sample.transaction do
s1.lock!
barrier.wait
- s2.update_attributes value: 1
+ s2.update value: 1
end
end
@@ -82,7 +84,7 @@ module ActiveRecord
Sample.transaction do
s2.lock!
barrier.wait
- s1.update_attributes value: 2
+ s1.update value: 2
end
ensure
thread.join
@@ -91,6 +93,90 @@ module ActiveRecord
end
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
+ latch2 = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch1.count_down
+ latch2.wait
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch1.wait
+ Sample.connection.execute("SET lock_timeout = 1")
+ Sample.lock.find(s.id)
+ end
+ ensure
+ Sample.connection.execute("SET lock_timeout = DEFAULT")
+ latch2.count_down
+ thread.join
+ end
+ end
+ end
+
+ test "raises QueryCanceled when statement timeout exceeded" do
+ assert_raises(ActiveRecord::QueryCanceled) do
+ s = Sample.create!(value: 1)
+ latch1 = Concurrent::CountDownLatch.new
+ latch2 = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch1.count_down
+ latch2.wait
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch1.wait
+ Sample.connection.execute("SET statement_timeout = 1")
+ Sample.lock.find(s.id)
+ end
+ ensure
+ Sample.connection.execute("SET statement_timeout = DEFAULT")
+ latch2.count_down
+ thread.join
+ end
+ end
+ end
+
+ test "raises QueryCanceled when canceling statement due to user request" do
+ assert_raises(ActiveRecord::QueryCanceled) do
+ s = Sample.create!(value: 1)
+ latch = Concurrent::CountDownLatch.new
+
+ thread = Thread.new do
+ Sample.transaction do
+ Sample.lock.find(s.id)
+ latch.count_down
+ sleep(0.5)
+ conn = Sample.connection
+ pid = conn.query_value("SELECT pid FROM pg_stat_activity WHERE query LIKE '% FOR UPDATE'")
+ conn.execute("SELECT pg_cancel_backend(#{pid})")
+ end
+ end
+
+ begin
+ Sample.transaction do
+ latch.wait
+ Sample.lock.find(s.id)
+ end
+ ensure
+ thread.join
+ end
+ end
+ end
+
private
def with_warning_suppression
diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
index 449023b6eb..8212ed4263 100644
--- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb
@@ -30,6 +30,6 @@ class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase
big_range = 0..123456789123456789
assert_raises(ActiveModel::RangeError) { int_range.serialize(big_range) }
- assert_equal "[0,123456789123456789]", bigint_range.serialize(big_range)
+ assert_equal "[0,123456789123456789]", @connection.type_cast(bigint_range.serialize(big_range))
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
index 466d281e85..71d07e2f4c 100644
--- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
@@ -82,7 +82,7 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
UUIDType.reset_column_information
column = UUIDType.columns_hash["thingy"]
- assert column.array?
+ assert_predicate column, :array?
assert_equal "{}", column.default
schema = dump_table_schema "uuid_data_type"
@@ -93,10 +93,10 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
column = UUIDType.columns_hash["guid"]
assert_equal :uuid, column.type
assert_equal "uuid", column.sql_type
- assert_not column.array?
+ assert_not_predicate column, :array?
type = UUIDType.type_for_attribute("guid")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_treat_blank_uuid_as_nil
@@ -178,7 +178,7 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase
duplicate = klass.new(guid: record.guid)
assert record.guid.present? # Ensure we actually are testing a UUID
- assert_not duplicate.valid?
+ assert_not_predicate duplicate, :valid?
end
end
@@ -222,68 +222,66 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase
connection.execute "DROP FUNCTION IF EXISTS my_uuid_generator();"
end
- if ActiveRecord::Base.connection.supports_extensions?
- def test_id_is_uuid
- assert_equal :uuid, UUID.columns_hash["id"].type
- assert UUID.primary_key
- end
+ def test_id_is_uuid
+ assert_equal :uuid, UUID.columns_hash["id"].type
+ assert UUID.primary_key
+ end
- def test_id_has_a_default
- u = UUID.create
- assert_not_nil u.id
- end
+ def test_id_has_a_default
+ u = UUID.create
+ assert_not_nil u.id
+ end
- def test_auto_create_uuid
- u = UUID.create
- u.reload
- assert_not_nil u.other_uuid
- end
+ def test_auto_create_uuid
+ u = UUID.create
+ u.reload
+ assert_not_nil u.other_uuid
+ end
- def test_pk_and_sequence_for_uuid_primary_key
- pk, seq = connection.pk_and_sequence_for("pg_uuids")
- assert_equal "id", pk
- assert_nil seq
- end
+ def test_pk_and_sequence_for_uuid_primary_key
+ pk, seq = connection.pk_and_sequence_for("pg_uuids")
+ assert_equal "id", pk
+ assert_nil seq
+ end
- def test_schema_dumper_for_uuid_primary_key
- schema = dump_table_schema "pg_uuids"
- assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: -> { "uuid_generate_v1\(\)" }/, schema)
- assert_match(/t\.uuid "other_uuid", default: -> { "uuid_generate_v4\(\)" }/, schema)
- end
+ def test_schema_dumper_for_uuid_primary_key
+ schema = dump_table_schema "pg_uuids"
+ assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: -> { "uuid_generate_v1\(\)" }/, schema)
+ assert_match(/t\.uuid "other_uuid", default: -> { "uuid_generate_v4\(\)" }/, schema)
+ end
+
+ def test_schema_dumper_for_uuid_primary_key_with_custom_default
+ schema = dump_table_schema "pg_uuids_2"
+ assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: -> { "my_uuid_generator\(\)" }/, schema)
+ assert_match(/t\.uuid "other_uuid_2", default: -> { "my_uuid_generator\(\)" }/, schema)
+ end
- def test_schema_dumper_for_uuid_primary_key_with_custom_default
- schema = dump_table_schema "pg_uuids_2"
- assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: -> { "my_uuid_generator\(\)" }/, schema)
- assert_match(/t\.uuid "other_uuid_2", default: -> { "my_uuid_generator\(\)" }/, schema)
+ def test_schema_dumper_for_uuid_primary_key_default
+ schema = dump_table_schema "pg_uuids_3"
+ if connection.supports_pgcrypto_uuid?
+ assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "gen_random_uuid\(\)" }/, schema)
+ else
+ assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema)
end
+ end
+
+ def test_schema_dumper_for_uuid_primary_key_default_in_legacy_migration
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
- def test_schema_dumper_for_uuid_primary_key_default
- schema = dump_table_schema "pg_uuids_3"
- if connection.supports_pgcrypto_uuid?
- assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "gen_random_uuid\(\)" }/, schema)
- else
- assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema)
+ migration = Class.new(ActiveRecord::Migration[5.0]) do
+ def version; 101 end
+ def migrate(x)
+ create_table("pg_uuids_4", id: :uuid)
end
- end
+ end.new
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
- def test_schema_dumper_for_uuid_primary_key_default_in_legacy_migration
- @verbose_was = ActiveRecord::Migration.verbose
- ActiveRecord::Migration.verbose = false
-
- migration = Class.new(ActiveRecord::Migration[5.0]) do
- def version; 101 end
- def migrate(x)
- create_table("pg_uuids_4", id: :uuid)
- end
- end.new
- ActiveRecord::Migrator.new(:up, [migration]).migrate
-
- schema = dump_table_schema "pg_uuids_4"
- assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema)
- ensure
- drop_table "pg_uuids_4"
- ActiveRecord::Migration.verbose = @verbose_was
- end
+ schema = dump_table_schema "pg_uuids_4"
+ assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema)
+ ensure
+ drop_table "pg_uuids_4"
+ ActiveRecord::Migration.verbose = @verbose_was
end
end
@@ -302,38 +300,36 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase
drop_table "pg_uuids"
end
- if ActiveRecord::Base.connection.supports_extensions?
- def test_id_allows_default_override_via_nil
- col_desc = connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default
- FROM pg_attribute a
- LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
- WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first
- assert_nil col_desc["default"]
- end
+ def test_id_allows_default_override_via_nil
+ col_desc = connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default
+ FROM pg_attribute a
+ LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
+ WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first
+ assert_nil col_desc["default"]
+ end
- def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil
- schema = dump_table_schema "pg_uuids"
- assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema)
- end
+ def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil
+ schema = dump_table_schema "pg_uuids"
+ assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema)
+ end
- def test_schema_dumper_for_uuid_primary_key_with_default_nil_in_legacy_migration
- @verbose_was = ActiveRecord::Migration.verbose
- ActiveRecord::Migration.verbose = false
-
- migration = Class.new(ActiveRecord::Migration[5.0]) do
- def version; 101 end
- def migrate(x)
- create_table("pg_uuids_4", id: :uuid, default: nil)
- end
- end.new
- ActiveRecord::Migrator.new(:up, [migration]).migrate
-
- schema = dump_table_schema "pg_uuids_4"
- assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: nil/, schema)
- ensure
- drop_table "pg_uuids_4"
- ActiveRecord::Migration.verbose = @verbose_was
- end
+ def test_schema_dumper_for_uuid_primary_key_with_default_nil_in_legacy_migration
+ @verbose_was = ActiveRecord::Migration.verbose
+ ActiveRecord::Migration.verbose = false
+
+ migration = Class.new(ActiveRecord::Migration[5.0]) do
+ def version; 101 end
+ def migrate(x)
+ create_table("pg_uuids_4", id: :uuid, default: nil)
+ end
+ end.new
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+
+ schema = dump_table_schema "pg_uuids_4"
+ assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: nil/, schema)
+ ensure
+ drop_table "pg_uuids_4"
+ ActiveRecord::Migration.verbose = @verbose_was
end
end
@@ -367,23 +363,21 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase
drop_table "pg_uuid_posts"
end
- if ActiveRecord::Base.connection.supports_extensions?
- def test_collection_association_with_uuid
- post = UuidPost.create!
- comment = post.uuid_comments.create!
- assert post.uuid_comments.find(comment.id)
- end
+ def test_collection_association_with_uuid
+ post = UuidPost.create!
+ comment = post.uuid_comments.create!
+ assert post.uuid_comments.find(comment.id)
+ end
- def test_find_with_uuid
- UuidPost.create!
- assert_raise ActiveRecord::RecordNotFound do
- UuidPost.find(123456)
- end
+ def test_find_with_uuid
+ UuidPost.create!
+ assert_raise ActiveRecord::RecordNotFound do
+ UuidPost.find(123456)
end
+ end
- def test_find_by_with_uuid
- UuidPost.create!
- assert_nil UuidPost.find_by(id: 789)
- end
+ def test_find_by_with_uuid
+ UuidPost.create!
+ assert_nil UuidPost.find_by(id: 789)
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
index 3b081d34e1..b6d2ccdb53 100644
--- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb
@@ -1,23 +1,23 @@
# frozen_string_literal: true
require "cases/helper"
-require "models/developer"
-require "models/computer"
+require "models/author"
+require "models/post"
class SQLite3ExplainTest < ActiveRecord::SQLite3TestCase
- fixtures :developers
+ fixtures :authors
def test_explain_for_one_query
- explain = Developer.where(id: 1).explain
- assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\? \[\["id", 1\]\]|1)), explain
- assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
+ explain = Author.where(id: 1).explain
+ assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\? \[\["id", 1\]\]|1)), explain
+ assert_match(/(SEARCH )?TABLE authors USING (INTEGER )?PRIMARY KEY/, explain)
end
def test_explain_with_eager_loading
- explain = Developer.where(id: 1).includes(:audit_logs).explain
- assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\? \[\["id", 1\]\]|1)), explain
- assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain)
- assert_match %r(EXPLAIN for: SELECT "audit_logs"\.\* FROM "audit_logs" WHERE "audit_logs"\."developer_id" = (?:\? \[\["developer_id", 1\]\]|1)), explain
- assert_match(/(SCAN )?TABLE audit_logs/, explain)
+ explain = Author.where(id: 1).includes(:posts).explain
+ assert_match %r(EXPLAIN for: SELECT "authors"\.\* FROM "authors" WHERE "authors"\."id" = (?:\? \[\["id", 1\]\]|1)), explain
+ assert_match(/(SEARCH )?TABLE authors USING (INTEGER )?PRIMARY KEY/, explain)
+ assert_match %r(EXPLAIN for: SELECT "posts"\.\* FROM "posts" WHERE "posts"\."author_id" = (?:\? \[\["author_id", 1\]\]|1)), explain
+ assert_match(/(SCAN )?TABLE posts/, explain)
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/json_test.rb b/activerecord/test/cases/adapters/sqlite3/json_test.rb
new file mode 100644
index 0000000000..6f247fcd22
--- /dev/null
+++ b/activerecord/test/cases/adapters/sqlite3/json_test.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "cases/json_shared_test_cases"
+
+class SQLite3JSONTest < ActiveRecord::SQLite3TestCase
+ include JSONSharedTestCases
+
+ def setup
+ super
+ @connection.create_table("json_data_type") do |t|
+ t.json "payload", default: {}
+ t.json "settings"
+ end
+ end
+
+ def test_default
+ @connection.add_column "json_data_type", "permissions", column_type, default: { "users": "read", "posts": ["read", "write"] }
+ klass.reset_column_information
+
+ assert_equal({ "users" => "read", "posts" => ["read", "write"] }, klass.column_defaults["permissions"])
+ assert_equal({ "users" => "read", "posts" => ["read", "write"] }, klass.new.permissions)
+ end
+
+ private
+ def column_type
+ :json
+ end
+end
diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index de422fad23..1c85ff5674 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -38,7 +38,7 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase
end
def test_type_cast_bigdecimal
- bd = BigDecimal.new "10.0"
+ bd = BigDecimal "10.0"
assert_equal bd.to_f, @conn.type_cast(bd)
end
@@ -55,4 +55,11 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase
assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value))
end
+
+ def test_quoted_time_normalizes_date_qualified_time
+ value = ::Time.utc(2018, 3, 11, 12, 30, 0, 999999)
+ type = ActiveRecord::Type::Time.new
+
+ assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value))
+ 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 2b51a32db6..7e0ce3a28e 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -269,14 +269,6 @@ module ActiveRecord
end
end
- def test_indexes_logs_name
- with_example_table do
- assert_logged [["PRAGMA index_list(\"ex\")", "SCHEMA", []]] do
- assert_deprecated { @conn.indexes("ex", "hello") }
- end
- end
- end
-
def test_table_exists_logs_name
with_example_table do
sql = <<-SQL
@@ -368,16 +360,126 @@ module ActiveRecord
end
end
+ class Barcode < ActiveRecord::Base
+ self.primary_key = "code"
+ end
+
+ def test_copy_table_with_existing_records_have_custom_primary_key
+ connection = Barcode.connection
+ connection.create_table(:barcodes, primary_key: "code", id: :string, limit: 42, force: true) do |t|
+ t.text :other_attr
+ end
+ code = "214fe0c2-dd47-46df-b53b-66090b3c1d40"
+ Barcode.create!(code: code, other_attr: "xxx")
+
+ connection.remove_column("barcodes", "other_attr")
+
+ assert_equal code, Barcode.first.id
+ ensure
+ Barcode.reset_column_information
+ end
+
+ def test_copy_table_with_composite_primary_keys
+ connection = Barcode.connection
+ connection.create_table(:barcodes, primary_key: ["region", "code"], force: true) do |t|
+ t.string :region
+ t.string :code
+ t.text :other_attr
+ end
+ region = "US"
+ code = "214fe0c2-dd47-46df-b53b-66090b3c1d40"
+ Barcode.create!(region: region, code: code, other_attr: "xxx")
+
+ connection.remove_column("barcodes", "other_attr")
+
+ assert_equal ["region", "code"], connection.primary_keys("barcodes")
+
+ barcode = Barcode.first
+ assert_equal region, barcode.region
+ assert_equal code, barcode.code
+ ensure
+ Barcode.reset_column_information
+ end
+
+ def test_custom_primary_key_in_create_table
+ connection = Barcode.connection
+ connection.create_table :barcodes, id: false, force: true do |t|
+ t.primary_key :id, :string
+ end
+
+ assert_equal "id", connection.primary_key("barcodes")
+
+ custom_pk = Barcode.columns_hash["id"]
+
+ assert_equal :string, custom_pk.type
+ assert_not custom_pk.null
+ ensure
+ Barcode.reset_column_information
+ end
+
+ def test_custom_primary_key_in_change_table
+ connection = Barcode.connection
+ connection.create_table :barcodes, id: false, force: true do |t|
+ t.integer :dummy
+ end
+ connection.change_table :barcodes do |t|
+ t.primary_key :id, :string
+ end
+
+ assert_equal "id", connection.primary_key("barcodes")
+
+ custom_pk = Barcode.columns_hash["id"]
+
+ assert_equal :string, custom_pk.type
+ assert_not custom_pk.null
+ ensure
+ Barcode.reset_column_information
+ end
+
+ def test_add_column_with_custom_primary_key
+ connection = Barcode.connection
+ connection.create_table :barcodes, id: false, force: true do |t|
+ t.integer :dummy
+ end
+ connection.add_column :barcodes, :id, :string, primary_key: true
+
+ assert_equal "id", connection.primary_key("barcodes")
+
+ custom_pk = Barcode.columns_hash["id"]
+
+ assert_equal :string, custom_pk.type
+ assert_not custom_pk.null
+ ensure
+ Barcode.reset_column_information
+ end
+
+ def test_remove_column_preserves_partial_indexes
+ connection = Barcode.connection
+ connection.create_table :barcodes, force: true do |t|
+ t.string :code
+ t.string :region
+ t.boolean :bool_attr
+
+ t.index :code, unique: true, where: :bool_attr, name: "partial"
+ end
+ connection.remove_column :barcodes, :region
+
+ index = connection.indexes("barcodes").find { |idx| idx.name == "partial" }
+ assert_equal "bool_attr", index.where
+ ensure
+ Barcode.reset_column_information
+ end
+
def test_supports_extensions
assert_not @conn.supports_extensions?, "does not support extensions"
end
def test_respond_to_enable_extension
- assert @conn.respond_to?(:enable_extension)
+ assert_respond_to @conn, :enable_extension
end
def test_respond_to_disable_extension
- assert @conn.respond_to?(:disable_extension)
+ assert_respond_to @conn, :disable_extension
end
def test_statement_closed
@@ -398,6 +500,10 @@ module ActiveRecord
end
end
+ def test_deprecate_valid_alter_table_type
+ assert_deprecated { @conn.valid_alter_table_type?(:string) }
+ end
+
private
def assert_logged(logs)
diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
index 42b3841d41..61002435a4 100644
--- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb
@@ -10,7 +10,7 @@ class SQLite3StatementPoolTest < ActiveRecord::SQLite3TestCase
assert_equal "bar", cache["foo"]
pid = fork {
- lookup = cache["foo"];
+ lookup = cache["foo"]
exit!(!lookup)
}
diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb
index 7f654ec6f6..fbdf2ada4b 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(RuntimeError) { customers(:david).balance.instance_eval { @amount = 20 } }
+ assert_raise(frozen_error_class) { 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 83974f327e..f05dcac7dd 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -48,7 +48,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" }
- assert_equal 7, ActiveRecord::Migrator::current_version
+ assert_equal 7, @connection.migration_context.current_version
end
def test_schema_define_w_table_name_prefix
@@ -64,7 +64,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase
t.column :flavor, :string
end
end
- assert_equal 7, ActiveRecord::Migrator::current_version
+ assert_equal 7, @connection.migration_context.current_version
ensure
ActiveRecord::Base.table_name_prefix = old_table_name_prefix
ActiveRecord::SchemaMigration.table_name = table_name
@@ -116,8 +116,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_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
+ assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
end
def test_timestamps_without_null_set_null_to_false_on_change_table
@@ -129,8 +129,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_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
+ assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
end
def test_timestamps_without_null_set_null_to_false_on_add_timestamps
@@ -139,7 +139,7 @@ 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_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null
+ assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null
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..52573021a5
--- /dev/null
+++ b/activerecord/test/cases/arel/attributes/attribute_test.rb
@@ -0,0 +1,1015 @@
+# 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 "with a range" 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
+
+ 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
+
+ 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
+
+ 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 "with a range" 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 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 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 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
+
+ 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")
+
+ refute 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
+ 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..380573494d
--- /dev/null
+++ b/activerecord/test/cases/arel/collectors/sql_string_test.rb
@@ -0,0 +1,47 @@
+# 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(bv)
+ table = Table.new(:users)
+ manager = Arel::SelectManager.new table
+ manager.where(table[:age].eq(bv))
+ manager.where(table[:name].eq(bv))
+ manager.ast
+ end
+
+ def test_compile
+ bv = Nodes::BindParam.new(1)
+ collector = collect ast_with_binds bv
+
+ sql = collector.compile ["hello", "world"]
+ assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql
+ end
+
+ def test_returned_sql_uses_utf8_encoding
+ bv = Nodes::BindParam.new(1)
+ collector = collect ast_with_binds bv
+
+ sql = collector.compile ["hello", "world"]
+ 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..17a5271039
--- /dev/null
+++ b/activerecord/test/cases/arel/delete_manager_test.rb
@@ -0,0 +1,52 @@
+# 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
+ 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..1f8612f799
--- /dev/null
+++ b/activerecord/test/cases/arel/helper.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require "rubygems"
+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 < Minitest::Test
+ 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
+
+ def assert_like(expected, actual)
+ assert_equal expected.gsub(/\s+/, " ").strip,
+ actual.gsub(/\s+/, " ").strip
+ 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
+ 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..ae10ccf56c
--- /dev/null
+++ b/activerecord/test/cases/arel/insert_manager_test.rb
@@ -0,0 +1,244 @@
+# 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 Values node" do
+ manager = Arel::InsertManager.new
+ values = manager.create_values %w{ a b }, %w{ c d }
+
+ assert_kind_of Arel::Nodes::Values, values
+ assert_equal %w{ a b }, values.left
+ assert_equal %w{ c d }, values.right
+ end
+
+ it "allows sql literals" do
+ manager = Arel::InsertManager.new
+ manager.into Table.new(:users)
+ manager.values = manager.create_values [Arel.sql("*")], %w{ a }
+ 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::Values.new [1]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" VALUES (1)
+ }
+ 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::Values.new [1, "aaron"]
+ manager.columns << table[:id]
+ manager.columns << table[:name]
+ manager.to_sql.must_be_like %{
+ INSERT INTO "users" ("id", "name") VALUES (1, 'aaron')
+ }
+ 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..eff54abd91
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/and_test.rb
@@ -0,0 +1,21 @@
+# 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
+ 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..1efb16222a
--- /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 !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..9bc55a155b
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/binary_test.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ 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
+ refute_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")
+
+ refute_equal eq.hash, neq.hash
+ 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..2c087e624e
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/case_test.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Nodes
+ 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
+ refute_same dolly.case, node.case
+
+ assert_equal dolly.conditions, node.conditions
+ refute_same dolly.conditions, node.conditions
+
+ assert_equal dolly.default, node.default
+ refute_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
+ 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/count_test.rb b/activerecord/test/cases/arel/nodes/count_test.rb
new file mode 100644
index 0000000000..3107659e77
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/count_test.rb
@@ -0,0 +1,44 @@
+# 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
+
+ describe "math" do
+ it "allows mathematical functions" do
+ table = Arel::Table.new :users
+ (table[:id].count + 1).to_sql.must_be_like %{
+ (COUNT("users"."id") + 1)
+ }
+ 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..45e22de17b
--- /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 !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..1cdc7a2360
--- /dev/null
+++ b/activerecord/test/cases/arel/nodes/select_core_test.rb
@@ -0,0 +1,71 @@
+# 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
+
+ refute_same core.froms, dolly.froms
+ refute_same core.projections, dolly.projections
+ refute_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]
+ 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]
+ 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]
+ 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]
+ 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..1bc9f6abf2
--- /dev/null
+++ b/activerecord/test/cases/arel/select_manager_test.rb
@@ -0,0 +1,1236 @@
+# 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 interect 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
+ refute_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
+ 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..8620d6fd34
--- /dev/null
+++ b/activerecord/test/cases/arel/support/fake_record.rb
@@ -0,0 +1,129 @@
+# 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.to_s}\""
+ end
+
+ def quote_column_name(name)
+ "\"#{name.to_s}\""
+ 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..3baccc7316
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/depth_first_test.rb
@@ -0,0 +1,271 @@
+# 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::Top,
+ 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
+
+ [
+ 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::Values,
+ 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..98f3bab620
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/dot_test.rb
@@ -0,0 +1,84 @@
+# 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::Top,
+ 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::Values,
+ 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..7163cb34d3
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/ibm_db_test.rb
@@ -0,0 +1,34 @@
+# 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
+ 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..b0b031cca3
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/informix_test.rb
@@ -0,0 +1,59 @@
+# 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
+ 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..340376c3d6
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/mssql_test.rb
@@ -0,0 +1,99 @@
+# 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
+ 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..9d3bad8516
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/mysql_test.rb
@@ -0,0 +1,80 @@
+# 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
+
+ it "squashes parenthesis on multiple unions" do
+ subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right")
+ node = Nodes::Union.new subnode, Arel.sql("topright")
+ assert_equal 1, compile(node).scan("(").length
+
+ subnode = Nodes::Union.new Arel.sql("left"), Arel.sql("right")
+ node = Nodes::Union.new Arel.sql("topleft"), subnode
+ assert_equal 1, compile(node).scan("(").length
+ end
+
+ ###
+ # :'(
+ # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214
+ 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
+ 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..83a2ee36ca
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/oracle12_test.rb
@@ -0,0 +1,61 @@
+# 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)
+ 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
+ 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..e1dfe40cf9
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/oracle_test.rb
@@ -0,0 +1,197 @@
+# 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)
+ 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
+ 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..ba37afecfb
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/postgres_test.rb
@@ -0,0 +1,281 @@
+# 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 paranthesis 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 SET( "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 SET( "users"."name", "users"."bool" )
+ }
+ end
+
+ it "should know how to generate paranthesis 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 SET( ( "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 paranthesis 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
+ 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..6650b6ff3a
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/sqlite_test.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+require_relative "../helper"
+
+module Arel
+ module Visitors
+ class SqliteTest < Arel::Spec
+ before do
+ @visitor = SQLite.new Table.engine.connection_pool
+ 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
+ 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..ce836eded7
--- /dev/null
+++ b/activerecord/test/cases/arel/visitors/to_sql_test.rb
@@ -0,0 +1,654 @@
+# 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 Values" do
+ bp = Nodes::BindParam.new(1)
+ values = Nodes::Values.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
+
+ 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_Bignum" 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 Concatination" 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::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 0f7a249bf3..a85b56ac4b 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -37,6 +37,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal companies(:first_firm).name, firm.name
end
+ def test_assigning_belongs_to_on_destroyed_object
+ client = Client.create!(name: "Client")
+ client.destroy!
+ assert_raise(frozen_error_class) { client.firm = nil }
+ assert_raise(frozen_error_class) { client.firm = Firm.new(name: "Firm") }
+ end
+
def test_missing_attribute_error_is_raised_when_no_foreign_key_attribute
assert_raises(ActiveModel::MissingAttributeError) { Client.select(:id).first.firm }
end
@@ -79,7 +86,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
account = model.new
- assert account.valid?
+ assert_predicate account, :valid?
ensure
ActiveRecord::Base.belongs_to_required_by_default = original_value
end
@@ -95,7 +102,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
account = model.new
- assert_not account.valid?
+ assert_not_predicate account, :valid?
assert_equal [{ error: :blank }], account.errors.details[:company]
ensure
ActiveRecord::Base.belongs_to_required_by_default = original_value
@@ -112,7 +119,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
account = model.new
- assert_not account.valid?
+ assert_not_predicate account, :valid?
assert_equal [{ error: :blank }], account.errors.details[:company]
ensure
ActiveRecord::Base.belongs_to_required_by_default = original_value
@@ -246,14 +253,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
Firm.create("name" => "Apple")
Client.create("name" => "Citibank", :firm_name => "Apple")
citibank_result = Client.all.merge!(where: { name: "Citibank" }, includes: :firm_with_primary_key).first
- assert citibank_result.association(:firm_with_primary_key).loaded?
+ assert_predicate citibank_result.association(:firm_with_primary_key), :loaded?
end
def test_eager_loading_with_primary_key_as_symbol
Firm.create("name" => "Apple")
Client.create("name" => "Citibank", :firm_name => "Apple")
citibank_result = Client.all.merge!(where: { name: "Citibank" }, includes: :firm_with_primary_key_symbols).first
- assert citibank_result.association(:firm_with_primary_key_symbols).loaded?
+ assert_predicate citibank_result.association(:firm_with_primary_key_symbols), :loaded?
end
def test_creating_the_belonging_object
@@ -320,7 +327,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
client = Client.create!(name: "Jimmy")
account = client.create_account!(credit_limit: 10)
assert_equal account, client.account
- assert account.persisted?
+ assert_predicate account, :persisted?
client.save
client.reload
assert_equal account, client.account
@@ -330,7 +337,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
client = Client.create!(name: "Jimmy")
assert_raise(ActiveRecord::RecordInvalid) { client.create_account! }
assert_not_nil client.account
- assert client.account.new_record?
+ assert_predicate client.account, :new_record?
end
def test_reloading_the_belonging_object
@@ -627,10 +634,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
final_cut = Client.new("name" => "Final Cut")
firm = Firm.find(1)
final_cut.firm = firm
- assert !final_cut.persisted?
+ assert_not_predicate final_cut, :persisted?
assert final_cut.save
- assert final_cut.persisted?
- assert firm.persisted?
+ assert_predicate final_cut, :persisted?
+ assert_predicate firm, :persisted?
assert_equal firm, final_cut.firm
final_cut.association(:firm).reload
assert_equal firm, final_cut.firm
@@ -640,10 +647,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
final_cut = Client.new("name" => "Final Cut")
firm = Firm.find(1)
final_cut.firm_with_primary_key = firm
- assert !final_cut.persisted?
+ assert_not_predicate final_cut, :persisted?
assert final_cut.save
- assert final_cut.persisted?
- assert firm.persisted?
+ assert_predicate final_cut, :persisted?
+ assert_predicate firm, :persisted?
assert_equal firm, final_cut.firm_with_primary_key
final_cut.association(:firm_with_primary_key).reload
assert_equal firm, final_cut.firm_with_primary_key
@@ -790,7 +797,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_cant_save_readonly_association
assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_client).readonly_firm.save! }
- assert companies(:first_client).readonly_firm.readonly?
+ assert_predicate companies(:first_client).readonly_firm, :readonly?
end
def test_polymorphic_assignment_foreign_key_type_string
@@ -931,6 +938,30 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal error.message, "The :dependent option must be one of [:destroy, :delete], but is :nullify"
end
+ class DestroyableBook < ActiveRecord::Base
+ self.table_name = "books"
+ belongs_to :author, class_name: "UndestroyableAuthor", dependent: :destroy
+ end
+
+ class UndestroyableAuthor < ActiveRecord::Base
+ self.table_name = "authors"
+ has_one :book, class_name: "DestroyableBook", foreign_key: "author_id"
+ before_destroy :dont
+
+ def dont
+ throw(:abort)
+ end
+ end
+
+ def test_dependency_should_halt_parent_destruction
+ author = UndestroyableAuthor.create!(name: "Test")
+ book = DestroyableBook.create!(author: author)
+
+ assert_no_difference ["UndestroyableAuthor.count", "DestroyableBook.count"] do
+ assert_not book.destroy
+ end
+ end
+
def test_attributes_are_being_set_when_initialized_from_belongs_to_association_with_where_clause
new_firm = accounts(:signals37).build_firm(name: "Apple")
assert_equal new_firm.name, "Apple"
@@ -949,15 +980,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
firm_proxy = client.send(:association_instance_get, :firm)
firm_with_condition_proxy = client.send(:association_instance_get, :firm_with_condition)
- assert !firm_proxy.stale_target?
- assert !firm_with_condition_proxy.stale_target?
+ assert_not_predicate firm_proxy, :stale_target?
+ assert_not_predicate firm_with_condition_proxy, :stale_target?
assert_equal companies(:first_firm), client.firm
assert_equal companies(:first_firm), client.firm_with_condition
client.client_of = companies(:another_firm).id
- assert firm_proxy.stale_target?
- assert firm_with_condition_proxy.stale_target?
+ assert_predicate firm_proxy, :stale_target?
+ assert_predicate firm_with_condition_proxy, :stale_target?
assert_equal companies(:another_firm), client.firm
assert_equal companies(:another_firm), client.firm_with_condition
end
@@ -968,12 +999,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
sponsor.sponsorable
proxy = sponsor.send(:association_instance_get, :sponsorable)
- assert !proxy.stale_target?
+ assert_not_predicate proxy, :stale_target?
assert_equal members(:groucho), sponsor.sponsorable
sponsor.sponsorable_id = members(:some_other_guy).id
- assert proxy.stale_target?
+ assert_predicate proxy, :stale_target?
assert_equal members(:some_other_guy), sponsor.sponsorable
end
@@ -983,12 +1014,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
sponsor.sponsorable
proxy = sponsor.send(:association_instance_get, :sponsorable)
- assert !proxy.stale_target?
+ assert_not_predicate proxy, :stale_target?
assert_equal members(:groucho), sponsor.sponsorable
sponsor.sponsorable_type = "Firm"
- assert proxy.stale_target?
+ assert_predicate proxy, :stale_target?
assert_equal companies(:first_firm), sponsor.sponsorable
end
@@ -1122,7 +1153,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
comment.post_id = 9223372036854775808 # out of range in the bigint
assert_nil comment.post
- assert_not comment.valid?
+ assert_not_predicate comment, :valid?
assert_equal [{ error: :blank }], comment.errors.details[:post]
end
@@ -1142,7 +1173,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
citibank.firm_id = apple.id.to_s
- assert !citibank.association(:firm).stale_target?
+ assert_not_predicate citibank.association(:firm), :stale_target?
end
def test_reflect_the_most_recent_change
diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb
index e096cd4a0b..25d55dc4c9 100644
--- a/activerecord/test/cases/associations/callbacks_test.rb
+++ b/activerecord/test/cases/associations/callbacks_test.rb
@@ -15,7 +15,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
@david = authors(:david)
@thinking = posts(:thinking)
@authorless = posts(:authorless)
- assert @david.post_log.empty?
+ assert_empty @david.post_log
end
def test_adding_macro_callbacks
@@ -96,7 +96,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
def test_has_and_belongs_to_many_add_callback
david = developers(:david)
ar = projects(:active_record)
- assert ar.developers_log.empty?
+ assert_empty ar.developers_log
ar.developers_with_callbacks << david
assert_equal ["before_adding#{david.id}", "after_adding#{david.id}"], ar.developers_log
ar.developers_with_callbacks << david
@@ -122,12 +122,12 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
assert_equal alice, dev
assert_not_nil new_dev
assert new_dev, "record should not have been saved"
- assert_not alice.new_record?
+ assert_not_predicate alice, :new_record?
end
def test_has_and_belongs_to_many_after_add_called_after_save
ar = projects(:active_record)
- assert ar.developers_log.empty?
+ assert_empty ar.developers_log
alice = Developer.new(name: "alice")
ar.developers_with_callbacks << alice
assert_equal "after_adding#{alice.id}", ar.developers_log.last
@@ -143,7 +143,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
david = developers(:david)
jamis = developers(:jamis)
activerecord = projects(:active_record)
- assert activerecord.developers_log.empty?
+ assert_empty activerecord.developers_log
activerecord.developers_with_callbacks.delete(david)
assert_equal ["before_removing#{david.id}", "after_removing#{david.id}"], activerecord.developers_log
@@ -154,7 +154,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
def test_has_and_belongs_to_many_does_not_fire_callbacks_on_clear
activerecord = projects(:active_record)
- assert activerecord.developers_log.empty?
+ assert_empty activerecord.developers_log
if activerecord.developers_with_callbacks.size == 0
activerecord.developers << developers(:david)
activerecord.developers << developers(:jamis)
@@ -163,7 +163,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
end
activerecord.developers_with_callbacks.flat_map { |d| ["before_removing#{d.id}", "after_removing#{d.id}"] }.sort
assert activerecord.developers_with_callbacks.clear
- assert_predicate activerecord.developers_log, :empty?
+ assert_empty activerecord.developers_log
end
def test_has_many_and_belongs_to_many_callbacks_for_save_on_parent
@@ -183,7 +183,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase
@david.unchangeable_posts << @authorless
rescue Exception
end
- assert @david.post_log.empty?
+ assert_empty @david.post_log
assert_not_includes @david.unchangeable_posts, @authorless
@david.reload
assert_not_includes @david.unchangeable_posts, @authorless
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index e69cfe5e52..e717621928 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -37,7 +37,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
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
- assert_equal 1, assert_no_queries { authors.size }
+ assert_equal 3, assert_no_queries { authors.size }
assert_equal 10, assert_no_queries { authors[0].comments.size }
end
@@ -136,7 +136,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
end
def test_eager_association_loading_with_multiple_stis_and_order
- author = Author.all.merge!(includes: { posts: [ :special_comments , :very_special_comment ] }, order: ["authors.name", "comments.body", "very_special_comments_posts.body"], where: "posts.id = 4").first
+ author = Author.all.merge!(includes: { posts: [ :special_comments, :very_special_comment ] }, order: ["authors.name", "comments.body", "very_special_comments_posts.body"], where: "posts.id = 4").first
assert_equal authors(:david), author
assert_no_queries do
author.posts.first.special_comments
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 8754889143..5fca972aee 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
@@ -8,37 +8,81 @@ module Namespaced
class Post < ActiveRecord::Base
self.table_name = "posts"
has_one :tagging, as: :taggable, class_name: "Tagging"
+
+ def self.polymorphic_name
+ sti_name
+ end
end
end
-class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase
+module PolymorphicFullStiClassNamesSharedTest
def setup
+ @old_store_full_sti_class = ActiveRecord::Base.store_full_sti_class
+ 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)
- @old = ActiveRecord::Base.store_full_sti_class
end
def teardown
- ActiveRecord::Base.store_full_sti_class = @old
+ ActiveRecord::Base.store_full_sti_class = @old_store_full_sti_class
+ end
+
+ def test_class_names
+ ActiveRecord::Base.store_full_sti_class = !store_full_sti_class
+ post = Namespaced::Post.find_by_title("Great stuff")
+ assert_nil post.tagging
+
+ ActiveRecord::Base.store_full_sti_class = store_full_sti_class
+ post = Namespaced::Post.find_by_title("Great stuff")
+ assert_equal @tagging, post.tagging
end
def test_class_names_with_includes
- ActiveRecord::Base.store_full_sti_class = false
+ ActiveRecord::Base.store_full_sti_class = !store_full_sti_class
post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff")
assert_nil post.tagging
- ActiveRecord::Base.store_full_sti_class = true
+ ActiveRecord::Base.store_full_sti_class = store_full_sti_class
post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff")
assert_equal @tagging, post.tagging
end
def test_class_names_with_eager_load
- ActiveRecord::Base.store_full_sti_class = false
+ ActiveRecord::Base.store_full_sti_class = !store_full_sti_class
post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff")
assert_nil post.tagging
- ActiveRecord::Base.store_full_sti_class = true
+ ActiveRecord::Base.store_full_sti_class = store_full_sti_class
post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff")
assert_equal @tagging, post.tagging
end
+
+ def test_class_names_with_find_by
+ post = Namespaced::Post.find_by_title("Great stuff")
+
+ ActiveRecord::Base.store_full_sti_class = !store_full_sti_class
+ assert_nil Tagging.find_by(taggable: post)
+
+ ActiveRecord::Base.store_full_sti_class = store_full_sti_class
+ assert_equal @tagging, Tagging.find_by(taggable: post)
+ end
+end
+
+class PolymorphicFullStiClassNamesTest < ActiveRecord::TestCase
+ include PolymorphicFullStiClassNamesSharedTest
+
+ private
+ def store_full_sti_class
+ true
+ end
+end
+
+class PolymorphicNonFullStiClassNamesTest < ActiveRecord::TestCase
+ include PolymorphicFullStiClassNamesSharedTest
+
+ private
+ def store_full_sti_class
+ false
+ end
end
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 559f0e9338..f46be8734b 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -284,7 +284,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_loading_from_an_association_that_has_a_hash_of_conditions
- assert !Author.all.merge!(includes: :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts.empty?
+ assert_not_empty Author.all.merge!(includes: :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts
end
def test_loading_with_no_associations
@@ -427,7 +427,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_order_string_with_quoted_table_name
quoted_posts_id = Comment.connection.quote_table_name("posts") + "." + Comment.connection.quote_column_name("id")
assert_nothing_raised do
- Comment.includes(:post).references(:posts).order(quoted_posts_id)
+ Comment.includes(:post).references(:posts).order(Arel.sql(quoted_posts_id))
end
end
@@ -787,7 +787,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
Tagging.create!(taggable_type: "Post", taggable_id: post2.id, tag: tag)
tag_with_includes = OrderedTag.includes(:tagged_posts).find(tag.id)
- assert_equal(tag_with_includes.taggings.map(&:taggable).map(&:title), tag_with_includes.tagged_posts.map(&:title))
+ assert_equal tag_with_includes.ordered_taggings.map(&:taggable).map(&:title), tag_with_includes.tagged_posts.map(&:title)
end
def test_eager_has_many_through_multiple_with_order
@@ -874,14 +874,14 @@ class EagerAssociationTest < ActiveRecord::TestCase
posts(:thinking, :sti_comments),
Post.all.merge!(
includes: [:author, :comments], where: { "authors.name" => "David" },
- order: "UPPER(posts.title)", limit: 2, offset: 1
+ order: Arel.sql("UPPER(posts.title)"), limit: 2, offset: 1
).to_a
)
assert_equal(
posts(:sti_post_and_comments, :sti_comments),
Post.all.merge!(
includes: [:author, :comments], where: { "authors.name" => "David" },
- order: "UPPER(posts.title) DESC", limit: 2, offset: 1
+ order: Arel.sql("UPPER(posts.title) DESC"), limit: 2, offset: 1
).to_a
)
end
@@ -891,14 +891,14 @@ class EagerAssociationTest < ActiveRecord::TestCase
posts(:thinking, :sti_comments),
Post.all.merge!(
includes: [:author, :comments], where: { "authors.name" => "David" },
- order: ["UPPER(posts.title)", "posts.id"], limit: 2, offset: 1
+ order: [Arel.sql("UPPER(posts.title)"), "posts.id"], limit: 2, offset: 1
).to_a
)
assert_equal(
posts(:sti_post_and_comments, :sti_comments),
Post.all.merge!(
includes: [:author, :comments], where: { "authors.name" => "David" },
- order: ["UPPER(posts.title) DESC", "posts.id"], limit: 2, offset: 1
+ order: [Arel.sql("UPPER(posts.title) DESC"), "posts.id"], limit: 2, offset: 1
).to_a
)
end
@@ -1073,7 +1073,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_load_with_sti_sharing_association
- assert_queries(2) do #should not do 1 query per subclass
+ assert_queries(2) do # should not do 1 query per subclass
Comment.includes(:post).to_a
end
end
@@ -1218,6 +1218,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
client = assert_queries(2) { Client.preload(:firm).find(c.id) }
assert_no_queries { assert_nil client.firm }
+ assert_equal c.client_of, client.client_of
end
def test_preloading_empty_belongs_to_polymorphic
@@ -1225,6 +1226,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
tagging = assert_queries(2) { Tagging.preload(:taggable).find(t.id) }
assert_no_queries { assert_nil tagging.taggable }
+ assert_equal t.taggable_id, tagging.taggable_id
end
def test_preloading_through_empty_belongs_to
@@ -1444,51 +1446,51 @@ class EagerAssociationTest < ActiveRecord::TestCase
test "preloading readonly association" do
# has-one
firm = Firm.where(id: "1").preload(:readonly_account).first!
- assert firm.readonly_account.readonly?
+ assert_predicate firm.readonly_account, :readonly?
# has_and_belongs_to_many
project = Project.where(id: "2").preload(:readonly_developers).first!
- assert project.readonly_developers.first.readonly?
+ assert_predicate project.readonly_developers.first, :readonly?
# has-many :through
david = Author.where(id: "1").preload(:readonly_comments).first!
- assert david.readonly_comments.first.readonly?
+ assert_predicate david.readonly_comments.first, :readonly?
end
test "eager-loading non-readonly association" do
# has_one
firm = Firm.where(id: "1").eager_load(:account).first!
- assert_not firm.account.readonly?
+ assert_not_predicate firm.account, :readonly?
# has_and_belongs_to_many
project = Project.where(id: "2").eager_load(:developers).first!
- assert_not project.developers.first.readonly?
+ assert_not_predicate project.developers.first, :readonly?
# has_many :through
david = Author.where(id: "1").eager_load(:comments).first!
- assert_not david.comments.first.readonly?
+ assert_not_predicate david.comments.first, :readonly?
# belongs_to
post = Post.where(id: "1").eager_load(:author).first!
- assert_not post.author.readonly?
+ assert_not_predicate post.author, :readonly?
end
test "eager-loading readonly association" do
# has-one
firm = Firm.where(id: "1").eager_load(:readonly_account).first!
- assert firm.readonly_account.readonly?
+ assert_predicate firm.readonly_account, :readonly?
# has_and_belongs_to_many
project = Project.where(id: "2").eager_load(:readonly_developers).first!
- assert project.readonly_developers.first.readonly?
+ assert_predicate project.readonly_developers.first, :readonly?
# has-many :through
david = Author.where(id: "1").eager_load(:readonly_comments).first!
- assert david.readonly_comments.first.readonly?
+ assert_predicate david.readonly_comments.first, :readonly?
# belongs_to
post = Post.where(id: "1").eager_load(:readonly_author).first!
- assert post.readonly_author.readonly?
+ assert_predicate post.readonly_author, :readonly?
end
test "preloading a polymorphic association with references to the associated table" do
@@ -1501,12 +1503,47 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal posts(:welcome), post
end
+ test "eager-loading with a polymorphic association won't work consistently" do
+ assert_raise(ActiveRecord::EagerLoadPolymorphicError) { authors(:david).essays.eager_load(:writer).to_a }
+ assert_raise(ActiveRecord::EagerLoadPolymorphicError) { authors(:david).essays.eager_load(:writer).count }
+ assert_raise(ActiveRecord::EagerLoadPolymorphicError) { authors(:david).essays.eager_load(:writer).exists? }
+ end
+
# CollectionProxy#reader is expensive, so the preloader avoids calling it.
test "preloading has_many_through association avoids calling association.reader" do
ActiveRecord::Associations::HasManyAssociation.any_instance.expects(:reader).never
Author.preload(:readonly_comments).first!
end
+ 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
def find_all_ordered(klass, include = nil)
klass.order("#{klass.table_name}.#{klass.primary_key}").includes(include).to_a
diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb
index 979dd986de..5d9735d98a 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
@@ -88,12 +88,6 @@ class DeveloperWithSymbolClassName < Developer
has_and_belongs_to_many :projects, class_name: :ProjectWithSymbolsForKeys
end
-ActiveSupport::Deprecation.silence do
- class DeveloperWithConstantClassName < Developer
- has_and_belongs_to_many :projects, class_name: ProjectWithSymbolsForKeys
- end
-end
-
class DeveloperWithExtendOption < Developer
module NamedExtension
def category
@@ -186,11 +180,11 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_has_and_belongs_to_many
david = Developer.find(1)
- assert !david.projects.empty?
+ assert_not_empty david.projects
assert_equal 2, david.projects.size
active_record = Project.find(1)
- assert !active_record.developers.empty?
+ assert_not_empty active_record.developers
assert_equal 3, active_record.developers.size
assert_includes active_record.developers, david
end
@@ -268,10 +262,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
no_of_projects = Project.count
aredridel = Developer.new("name" => "Aredridel")
aredridel.projects.concat([Project.find(1), p = Project.new("name" => "Projekt")])
- assert !aredridel.persisted?
- assert !p.persisted?
+ assert_not_predicate aredridel, :persisted?
+ assert_not_predicate p, :persisted?
assert aredridel.save
- assert aredridel.persisted?
+ assert_predicate aredridel, :persisted?
assert_equal no_of_devels + 1, Developer.count
assert_equal no_of_projects + 1, Project.count
assert_equal 2, aredridel.projects.size
@@ -317,14 +311,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_build
devel = Developer.find(1)
proj = assert_no_queries(ignore_none: false) { devel.projects.build("name" => "Projekt") }
- assert !devel.projects.loaded?
+ assert_not_predicate devel.projects, :loaded?
assert_equal devel.projects.last, proj
- assert devel.projects.loaded?
+ assert_predicate devel.projects, :loaded?
- assert !proj.persisted?
+ assert_not_predicate proj, :persisted?
devel.save
- assert proj.persisted?
+ assert_predicate proj, :persisted?
assert_equal devel.projects.last, proj
assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
end
@@ -332,14 +326,14 @@ 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") }
- assert !devel.projects.loaded?
+ assert_not_predicate devel.projects, :loaded?
assert_equal devel.projects.last, proj
- assert devel.projects.loaded?
+ assert_predicate devel.projects, :loaded?
- assert !proj.persisted?
+ assert_not_predicate proj, :persisted?
devel.save
- assert proj.persisted?
+ assert_predicate proj, :persisted?
assert_equal devel.projects.last, proj
assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
end
@@ -349,10 +343,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
devel.projects.build(name: "Make bed")
proj2 = devel.projects.build(name: "Lie in it")
assert_equal devel.projects.last, proj2
- assert !proj2.persisted?
+ assert_not_predicate proj2, :persisted?
devel.save
- assert devel.persisted?
- assert proj2.persisted?
+ assert_predicate devel, :persisted?
+ assert_predicate proj2, :persisted?
assert_equal devel.projects.last, proj2
assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated
end
@@ -360,12 +354,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_create
devel = Developer.find(1)
proj = devel.projects.create("name" => "Projekt")
- assert !devel.projects.loaded?
+ assert_not_predicate devel.projects, :loaded?
assert_equal devel.projects.last, proj
- assert !devel.projects.loaded?
+ assert_not_predicate devel.projects, :loaded?
- assert proj.persisted?
+ assert_predicate proj, :persisted?
assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated
end
@@ -373,14 +367,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
# in Oracle '' is saved as null therefore need to save ' ' in not null column
post = categories(:general).post_with_conditions.build(body: " ")
- assert post.save
- assert_equal "Yet Another Testing Title", post.title
+ assert post.save
+ assert_equal "Yet Another Testing Title", post.title
# in Oracle '' is saved as null therefore need to save ' ' in not null column
another_post = categories(:general).post_with_conditions.create(body: " ")
- assert another_post.persisted?
- assert_equal "Yet Another Testing Title", another_post.title
+ assert_predicate another_post, :persisted?
+ assert_equal "Yet Another Testing Title", another_post.title
end
def test_distinct_after_the_fact
@@ -447,10 +441,10 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_removing_associations_on_destroy
david = DeveloperWithBeforeDestroyRaise.find(1)
- assert !david.projects.empty?
+ assert_not_empty david.projects
david.destroy
- assert david.projects.empty?
- assert DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1").empty?
+ assert_empty david.projects
+ assert_empty DeveloperWithBeforeDestroyRaise.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = 1")
end
def test_destroying
@@ -465,7 +459,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id} AND project_id = #{project.id}")
- assert join_records.empty?
+ assert_empty join_records
assert_equal 1, david.reload.projects.size
assert_equal 1, david.projects.reload.size
@@ -481,7 +475,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id}")
- assert join_records.empty?
+ assert_empty join_records
assert_equal 0, david.reload.projects.size
assert_equal 0, david.projects.reload.size
@@ -490,23 +484,23 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_destroy_all
david = Developer.find(1)
david.projects.reload
- assert !david.projects.empty?
+ assert_not_empty david.projects
assert_no_difference "Project.count" do
david.projects.destroy_all
end
join_records = Developer.connection.select_all("SELECT * FROM developers_projects WHERE developer_id = #{david.id}")
- assert join_records.empty?
+ assert_empty join_records
- assert david.projects.empty?
- assert david.projects.reload.empty?
+ assert_empty david.projects
+ assert_empty david.projects.reload
end
def test_destroy_associations_destroys_multiple_associations
george = parrots(:george)
- assert !george.pirates.empty?
- assert !george.treasures.empty?
+ assert_not_empty george.pirates
+ assert_not_empty george.treasures
assert_no_difference "Pirate.count" do
assert_no_difference "Treasure.count" do
@@ -515,12 +509,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
join_records = Parrot.connection.select_all("SELECT * FROM parrots_pirates WHERE parrot_id = #{george.id}")
- assert join_records.empty?
- assert george.pirates.reload.empty?
+ assert_empty join_records
+ assert_empty george.pirates.reload
join_records = Parrot.connection.select_all("SELECT * FROM parrots_treasures WHERE parrot_id = #{george.id}")
- assert join_records.empty?
- assert george.treasures.reload.empty?
+ assert_empty join_records
+ assert_empty george.treasures.reload
end
def test_associations_with_conditions
@@ -553,7 +547,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer = project.developers.first
assert_no_queries(ignore_none: false) do
- assert project.developers.loaded?
+ assert_predicate project.developers, :loaded?
assert_includes project.developers, developer
end
end
@@ -563,19 +557,19 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
developer = project.developers.first
project.reload
- assert ! project.developers.loaded?
+ assert_not_predicate project.developers, :loaded?
assert_queries(1) do
assert_includes project.developers, developer
end
- assert ! project.developers.loaded?
+ assert_not_predicate project.developers, :loaded?
end
def test_include_returns_false_for_non_matching_record_to_verify_scoping
project = projects(:active_record)
developer = Developer.create name: "Bryan", salary: 50_000
- assert ! project.developers.loaded?
- assert ! project.developers.include?(developer)
+ assert_not_predicate project.developers, :loaded?
+ assert_not project.developers.include?(developer)
end
def test_find_with_merged_options
@@ -668,7 +662,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_includes developer.sym_special_projects, sp
end
- def test_update_attributes_after_push_without_duplicate_join_table_rows
+ def test_update_columns_after_push_without_duplicate_join_table_rows
developer = Developer.new("name" => "Kano")
project = SpecialProject.create("name" => "Special Project")
assert developer.save
@@ -769,9 +763,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_unloaded_associations_does_not_load_them
developer = developers(:david)
- assert !developer.projects.loaded?
+ assert_not_predicate developer.projects, :loaded?
assert_equal projects(:active_record, :action_controller).map(&:id).sort, developer.project_ids.sort
- assert !developer.projects.loaded?
+ assert_not_predicate developer.projects, :loaded?
end
def test_assign_ids
@@ -954,13 +948,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
end
- def test_with_constant_class_name
- assert_nothing_raised do
- developer = DeveloperWithConstantClassName.new
- developer.projects
- end
- end
-
def test_alternate_database
professor = Professor.create(name: "Plum")
course = Course.create(name: "Forensics")
diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb
index 6bd11a5d81..bf2e051def 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -57,7 +57,7 @@ class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase
def test_custom_primary_key_on_new_record_should_fetch_with_query
subscriber = Subscriber.new(nick: "webster132")
- assert !subscriber.subscriptions.loaded?
+ assert_not_predicate subscriber.subscriptions, :loaded?
assert_queries 1 do
assert_equal 2, subscriber.subscriptions.size
@@ -68,7 +68,7 @@ class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase
def test_association_primary_key_on_new_record_should_fetch_with_query
author = Author.new(name: "David")
- assert !author.essays.loaded?
+ assert_not_predicate author.essays, :loaded?
assert_queries 1 do
assert_equal 1, author.essays.size
@@ -103,7 +103,7 @@ class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase
def test_blank_custom_primary_key_on_new_record_should_not_run_queries
author = Author.new
- assert !author.essays.loaded?
+ assert_not_predicate author.essays, :loaded?
assert_queries 0 do
assert_equal 0, author.essays.size
@@ -201,7 +201,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
part.reload
assert_nil part.ship
- assert !part.updated_at_changed?
+ assert_not_predicate part, :updated_at_changed?
end
def test_create_from_association_should_respect_default_scope
@@ -444,7 +444,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
new_clients << company.clients_of_firm.build(name: "Another Client III")
end
- assert_not company.clients_of_firm.loaded?
+ assert_not_predicate company.clients_of_firm, :loaded?
assert_queries(1) do
assert_same new_clients[0], company.clients_of_firm.third
assert_same new_clients[1], company.clients_of_firm.fourth
@@ -464,7 +464,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
new_clients << company.clients_of_firm.build(name: "Another Client III")
end
- assert_not company.clients_of_firm.loaded?
+ assert_not_predicate company.clients_of_firm, :loaded?
assert_queries(1) do
assert_same new_clients[0], company.clients_of_firm.third!
assert_same new_clients[1], company.clients_of_firm.fourth!
@@ -497,8 +497,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
person = Person.new
person.first_name = "Naruto"
person.references << Reference.new
- person.id = 10
- person.references
person.save!
assert_equal 1, person.references.update_all(favourite: true)
end
@@ -507,8 +505,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
person = Person.new
person.first_name = "Sasuke"
person.references << Reference.new
- person.id = 10
- person.references
person.save!
assert_predicate person.references, :exists?
end
@@ -587,14 +583,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
# taking from unloaded Relation
bob = klass.find(authors(:bob).id)
new_post = bob.posts.build
- assert_not bob.posts.loaded?
+ assert_not_predicate bob.posts, :loaded?
assert_equal [posts(:misc_by_bob)], bob.posts.take(1)
assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2)
assert_equal [posts(:misc_by_bob), posts(:other_by_bob), new_post], bob.posts.take(3)
# taking from loaded Relation
bob.posts.load
- assert bob.posts.loaded?
+ assert_predicate bob.posts, :loaded?
assert_equal [posts(:misc_by_bob)], bob.posts.take(1)
assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2)
assert_equal [posts(:misc_by_bob), posts(:other_by_bob), new_post], bob.posts.take(3)
@@ -713,13 +709,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_find_each
firm = companies(:first_firm)
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_queries(4) do
firm.clients.find_each(batch_size: 1) { |c| assert_equal firm.id, c.firm_id }
end
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_find_each_with_conditions
@@ -732,13 +728,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_find_in_batches
firm = companies(:first_firm)
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_queries(2) do
firm.clients.find_in_batches(batch_size: 2) do |clients|
@@ -746,7 +742,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_find_all_sanitized
@@ -831,7 +827,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_find_scoped_grouped_having
- assert_equal 1, authors(:david).popular_grouped_posts.length
+ assert_equal 2, authors(:david).popular_grouped_posts.length
assert_equal 0, authors(:mary).popular_grouped_posts.length
end
@@ -955,20 +951,20 @@ 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") }
- assert !company.clients_of_firm.loaded?
+ assert_not_predicate company.clients_of_firm, :loaded?
assert_equal "Another Client", new_client.name
- assert !new_client.persisted?
+ assert_not_predicate new_client, :persisted?
assert_equal new_client, company.clients_of_firm.last
end
def test_build
company = companies(:first_firm)
new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") }
- assert !company.clients_of_firm.loaded?
+ assert_not_predicate company.clients_of_firm, :loaded?
assert_equal "Another Client", new_client.name
- assert !new_client.persisted?
+ assert_not_predicate new_client, :persisted?
assert_equal new_client, company.clients_of_firm.last
end
@@ -982,9 +978,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_collection_not_empty_after_building
company = companies(:first_firm)
- assert_predicate company.contracts, :empty?
+ assert_empty company.contracts
company.contracts.build
- assert_not_predicate company.contracts, :empty?
+ 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
@@ -1008,7 +1024,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_build_followed_by_save_does_not_load_target
companies(:first_firm).clients_of_firm.build("name" => "Another Client")
assert companies(:first_firm).save
- assert !companies(:first_firm).clients_of_firm.loaded?
+ assert_not_predicate companies(:first_firm).clients_of_firm, :loaded?
end
def test_build_without_loading_association
@@ -1028,10 +1044,10 @@ 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" } }
- assert !company.clients_of_firm.loaded?
+ assert_not_predicate company.clients_of_firm, :loaded?
assert_equal "Another Client", new_client.name
- assert !new_client.persisted?
+ assert_not_predicate new_client, :persisted?
assert_equal new_client, company.clients_of_firm.last
end
@@ -1069,7 +1085,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_predicate companies(:first_firm).clients_of_firm, :loaded?
new_client = companies(:first_firm).clients_of_firm.create("name" => "Another Client")
- assert new_client.persisted?
+ assert_predicate new_client, :persisted?
assert_equal new_client, companies(:first_firm).clients_of_firm.last
assert_equal new_client, companies(:first_firm).clients_of_firm.reload.last
end
@@ -1082,7 +1098,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_create_followed_by_save_does_not_load_target
companies(:first_firm).clients_of_firm.create("name" => "Another Client")
assert companies(:first_firm).save
- assert !companies(:first_firm).clients_of_firm.loaded?
+ assert_not_predicate companies(:first_firm).clients_of_firm, :loaded?
end
def test_deleting
@@ -1108,7 +1124,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
# option is not given on the association.
ship = Ship.create(name: "Countless", treasures_count: 10)
- assert_not Ship.reflect_on_association(:treasures).has_cached_counter?
+ assert_not_predicate Ship.reflect_on_association(:treasures), :has_cached_counter?
# Count should come from sql count() of treasures rather than treasures_count attribute
assert_equal ship.treasures.size, 0
@@ -1199,7 +1215,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_empty_with_counter_cache
post = posts(:welcome)
assert_queries(0) do
- assert_not post.comments.empty?
+ assert_not_empty post.comments
end
end
@@ -1211,20 +1227,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- def test_calling_update_attributes_on_id_changes_the_counter_cache
+ def test_calling_update_on_id_changes_the_counter_cache
topic = Topic.order("id ASC").first
original_count = topic.replies.to_a.size
assert_equal original_count, topic.replies_count
first_reply = topic.replies.first
- first_reply.update_attributes(parent_id: nil)
+ first_reply.update(parent_id: nil)
assert_equal original_count - 1, topic.reload.replies_count
- first_reply.update_attributes(parent_id: topic.id)
+ first_reply.update(parent_id: topic.id)
assert_equal original_count, topic.reload.replies_count
end
- def test_calling_update_attributes_changing_ids_doesnt_change_counter_cache
+ def test_calling_update_changing_ids_doesnt_change_counter_cache
topic1 = Topic.find(1)
topic2 = Topic.find(3)
original_count1 = topic1.replies.to_a.size
@@ -1233,11 +1249,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
reply1 = topic1.replies.first
reply2 = topic2.replies.first
- reply1.update_attributes(parent_id: topic2.id)
+ reply1.update(parent_id: topic2.id)
assert_equal original_count1 - 1, topic1.reload.replies_count
assert_equal original_count2 + 1, topic2.reload.replies_count
- reply2.update_attributes(parent_id: topic1.id)
+ reply2.update(parent_id: topic1.id)
assert_equal original_count1, topic1.reload.replies_count
assert_equal original_count2, topic2.reload.replies_count
end
@@ -1441,13 +1457,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_creation_respects_hash_condition
ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.build
- assert ms_client.save
- assert_equal "Microsoft", ms_client.name
+ assert ms_client.save
+ assert_equal "Microsoft", ms_client.name
another_ms_client = companies(:first_firm).clients_like_ms_with_hash_conditions.create
- assert another_ms_client.persisted?
- assert_equal "Microsoft", another_ms_client.name
+ assert_predicate another_ms_client, :persisted?
+ assert_equal "Microsoft", another_ms_client.name
end
def test_clearing_without_initial_access
@@ -1570,7 +1586,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm = companies(:first_firm)
assert_equal 3, firm.clients.size
firm.destroy
- assert Client.all.merge!(where: "firm_id=#{firm.id}").to_a.empty?
+ assert_empty Client.all.merge!(where: "firm_id=#{firm.id}").to_a
end
def test_dependence_for_associations_with_hash_condition
@@ -1633,7 +1649,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm = RestrictedWithExceptionFirm.create!(name: "restrict")
firm.companies.create(name: "child")
- assert !firm.companies.empty?
+ assert_not_empty firm.companies
assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
assert RestrictedWithExceptionFirm.exists?(name: "restrict")
assert firm.companies.exists?(name: "child")
@@ -1643,11 +1659,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm = RestrictedWithErrorFirm.create!(name: "restrict")
firm.companies.create(name: "child")
- assert !firm.companies.empty?
+ assert_not_empty firm.companies
firm.destroy
- assert !firm.errors.empty?
+ assert_not_empty firm.errors
assert_equal "Cannot delete record because dependent companies exist", firm.errors[:base].first
assert RestrictedWithErrorFirm.exists?(name: "restrict")
@@ -1660,11 +1676,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm = RestrictedWithErrorFirm.create!(name: "restrict")
firm.companies.create(name: "child")
- assert !firm.companies.empty?
+ assert_not_empty firm.companies
firm.destroy
- assert !firm.errors.empty?
+ assert_not_empty firm.errors
assert_equal "Cannot delete record because dependent client companies exist", firm.errors[:base].first
assert RestrictedWithErrorFirm.exists?(name: "restrict")
@@ -1716,8 +1732,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
account = Account.new
orig_accounts = firm.accounts.to_a
- assert !account.valid?
- assert !orig_accounts.empty?
+ assert_not_predicate account, :valid?
+ assert_not_empty orig_accounts
error = assert_raise ActiveRecord::RecordNotSaved do
firm.accounts = [account]
end
@@ -1775,9 +1791,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_unloaded_associations_does_not_load_them
company = companies(:first_firm)
- assert !company.clients.loaded?
+ assert_not_predicate company.clients, :loaded?
assert_equal [companies(:first_client).id, companies(:second_client).id, companies(:another_first_firm_client).id], company.client_ids
- assert !company.clients.loaded?
+ assert_not_predicate company.clients, :loaded?
end
def test_counter_cache_on_unloaded_association
@@ -1842,6 +1858,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
].each { |block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) }
end
+ def test_associations_order_should_be_priority_over_throughs_order
+ david = 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)
+ end
+
def test_dynamic_find_should_respect_association_order_for_through
assert_equal Comment.find(10), authors(:david).comments_desc.where("comments.type = 'SpecialComment'").first
assert_equal Comment.find(10), authors(:david).comments_desc.find_by_type("SpecialComment")
@@ -1859,7 +1882,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
client = firm.clients.first
assert_no_queries do
- assert firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
assert_equal true, firm.clients.include?(client)
end
end
@@ -1869,18 +1892,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
client = firm.clients.first
firm.reload
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_queries(1) do
assert_equal true, firm.clients.include?(client)
end
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_include_returns_false_for_non_matching_record_to_verify_scoping
firm = companies(:first_firm)
client = Client.create!(name: "Not Associated")
- assert ! firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_equal false, firm.clients.include?(client)
end
@@ -1889,13 +1912,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients.first
firm.clients.second
firm.clients.last
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_calling_first_or_last_on_loaded_association_should_not_fetch_with_query
firm = companies(:first_firm)
firm.clients.load_target
- assert firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
assert_no_queries(ignore_none: false) do
firm.clients.first
@@ -1908,7 +1931,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_first_or_last_on_existing_record_with_build_should_load_association
firm = companies(:first_firm)
firm.clients.build(name: "Foo")
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_queries 1 do
firm.clients.first
@@ -1916,13 +1939,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients.last
end
- assert firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
end
def test_calling_first_nth_or_last_on_existing_record_with_create_should_not_load_association
firm = companies(:first_firm)
firm.clients.create(name: "Foo")
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_queries 3 do
firm.clients.first
@@ -1930,7 +1953,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.clients.last
end
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_calling_first_nth_or_last_on_new_record_should_not_run_queries
@@ -1946,14 +1969,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_calling_first_or_last_with_integer_on_association_should_not_load_association
firm = companies(:first_firm)
firm.clients.create(name: "Foo")
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
assert_queries 2 do
firm.clients.first(2)
firm.clients.last(2)
end
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_calling_many_should_count_instead_of_loading_association
@@ -1961,7 +1984,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_queries(1) do
firm.clients.many? # use count query
end
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
def test_calling_many_on_loaded_association_should_not_use_query
@@ -1973,25 +1996,26 @@ 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 firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
end
def test_calling_many_should_return_false_if_none_or_one
firm = companies(:another_firm)
- assert !firm.clients_like_ms.many?
+ assert_not_predicate firm.clients_like_ms, :many?
assert_equal 0, firm.clients_like_ms.size
firm = companies(:first_firm)
- assert !firm.limited_clients.many?
+ assert_not_predicate firm.limited_clients, :many?
assert_equal 1, firm.limited_clients.size
end
def test_calling_many_should_return_true_if_more_than_one
firm = companies(:first_firm)
- assert firm.clients.many?
+ assert_predicate firm.clients, :many?
assert_equal 3, firm.clients.size
end
@@ -2000,33 +2024,34 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_queries(1) do
firm.clients.none? # use count query
end
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
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 firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
end
def test_calling_none_should_return_true_if_none
firm = companies(:another_firm)
- assert firm.clients_like_ms.none?
+ assert_predicate firm.clients_like_ms, :none?
assert_equal 0, firm.clients_like_ms.size
end
def test_calling_none_should_return_false_if_any
firm = companies(:first_firm)
- assert !firm.limited_clients.none?
+ assert_not_predicate firm.limited_clients, :none?
assert_equal 1, firm.limited_clients.size
end
@@ -2035,39 +2060,40 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_queries(1) do
firm.clients.one? # use count query
end
- assert !firm.clients.loaded?
+ assert_not_predicate firm.clients, :loaded?
end
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 firm.clients.loaded?
+ assert_predicate firm.clients, :loaded?
end
def test_calling_one_should_return_false_if_zero
firm = companies(:another_firm)
- assert ! firm.clients_like_ms.one?
+ assert_not_predicate firm.clients_like_ms, :one?
assert_equal 0, firm.clients_like_ms.size
end
def test_calling_one_should_return_true_if_one
firm = companies(:first_firm)
- assert firm.limited_clients.one?
+ assert_predicate firm.limited_clients, :one?
assert_equal 1, firm.limited_clients.size
end
def test_calling_one_should_return_false_if_more_than_one
firm = companies(:first_firm)
- assert ! firm.clients.one?
+ assert_not_predicate firm.clients, :one?
assert_equal 3, firm.clients.size
end
@@ -2089,9 +2115,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
@@ -2287,7 +2314,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
post = posts(:welcome)
assert post.taggings_with_delete_all.count > 0
- assert !post.taggings_with_delete_all.loaded?
+ assert_not_predicate post.taggings_with_delete_all, :loaded?
# 2 queries: one DELETE and another to update the counter cache
assert_queries(2) do
@@ -2309,7 +2336,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
test "collection proxy respects default scope" do
author = authors(:mary)
- assert !author.first_posts.exists?
+ assert_not_predicate author.first_posts, :exists?
end
test "association with extend option" do
@@ -2350,7 +2377,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
test "does not duplicate associations when used with natural primary keys" do
speedometer = Speedometer.create!(id: "4")
- speedometer.minivans.create!(minivan_id: "a-van-red" , name: "a van", color: "red")
+ speedometer.minivans.create!(minivan_id: "a-van-red", name: "a van", color: "red")
assert_equal 1, speedometer.minivans.to_a.size, "Only one association should be present:\n#{speedometer.minivans.to_a}"
assert_equal 1, speedometer.reload.minivans.to_a.size
@@ -2428,7 +2455,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
pirate = FamousPirate.new
pirate.famous_ships << ship = FamousShip.new
- assert pirate.valid?
+ assert_predicate pirate, :valid?
assert_not pirate.valid?(:conference)
assert_equal "can't be blank", ship.errors[:name].first
end
@@ -2509,6 +2536,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_same car, new_bulb.car
end
+ test "reattach to new objects replaces inverse association and foreign key" do
+ bulb = Bulb.create!(car: Car.create!)
+ assert bulb.car_id
+ car = Car.new
+ car.bulbs << bulb
+ assert_equal car, bulb.car
+ assert_nil bulb.car_id
+ end
+
test "in memory replacement maintains order" do
first_bulb = Bulb.create!
second_bulb = Bulb.create!
@@ -2520,6 +2556,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [first_bulb, second_bulb], car.bulbs
end
+ test "association size calculation works with default scoped selects when not previously fetched" do
+ firm = Firm.create!(name: "Firm")
+ 5.times { firm.developers_with_select << Developer.create!(name: "Developer") }
+
+ same_firm = Firm.find(firm.id)
+ assert_equal 5, same_firm.developers_with_select.size
+ end
+
test "prevent double insertion of new object when the parent association loaded in the after save callback" do
reset_callbacks(:save, Bulb) do
Bulb.after_save { |record| record.car.bulbs.load }
@@ -2543,6 +2587,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,
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 521b388cee..0facc286da 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -46,6 +46,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
Reader.create person_id: 0, post_id: 0
end
+ def test_marshal_dump
+ preloaded = Post.includes(:first_blue_tags).first
+ assert_equal preloaded, Marshal.load(Marshal.dump(preloaded))
+ end
+
def test_preload_sti_rhs_class
developers = Developer.includes(:firms).all.to_a
assert_no_queries do
@@ -353,10 +358,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
assert_queries(1) do
- assert posts(:welcome).people.empty?
+ assert_empty posts(:welcome).people
end
- assert posts(:welcome).reload.people.reload.empty?
+ assert_empty posts(:welcome).reload.people.reload
end
def test_destroy_association
@@ -366,8 +371,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
- assert posts(:welcome).reload.people.empty?
- assert posts(:welcome).people.reload.empty?
+ assert_empty posts(:welcome).reload.people
+ assert_empty posts(:welcome).people.reload
end
def test_destroy_all
@@ -377,8 +382,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
- assert posts(:welcome).reload.people.empty?
- assert posts(:welcome).people.reload.empty?
+ assert_empty posts(:welcome).reload.people
+ assert_empty posts(:welcome).people.reload
end
def test_should_raise_exception_for_destroying_mismatching_records
@@ -538,6 +543,16 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_update_counter_caches_on_destroy_with_indestructible_through_record
+ post = posts(:welcome)
+ tag = post.indestructible_tags.create!(name: "doomed")
+ post.update_columns(indestructible_tags_count: post.indestructible_tags.count)
+
+ assert_no_difference "post.reload.indestructible_tags_count" do
+ posts(:welcome).indestructible_tags.destroy(tag)
+ end
+ end
+
def test_replace_association
assert_queries(4) { posts(:welcome);people(:david);people(:michael); posts(:welcome).people.reload }
@@ -675,10 +690,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
assert_queries(0) do
- assert posts(:welcome).people.empty?
+ assert_empty posts(:welcome).people
end
- assert posts(:welcome).reload.people.reload.empty?
+ assert_empty posts(:welcome).reload.people.reload
end
def test_association_callback_ordering
@@ -760,9 +775,9 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_get_ids_for_unloaded_associations_does_not_load_them
person = people(:michael)
- assert !person.posts.loaded?
+ assert_not_predicate person.posts, :loaded?
assert_equal [posts(:welcome).id, posts(:authorless).id].sort, person.post_ids.sort
- assert !person.posts.loaded?
+ assert_not_predicate person.posts, :loaded?
end
def test_association_proxy_transaction_method_starts_transaction_in_association_class
@@ -851,8 +866,8 @@ 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 author.named_categories.reload.empty?
+ assert_not Categorization.exists?(author_id: author.id, named_category_name: category.name)
+ assert_empty author.named_categories.reload
end
def test_collection_singular_ids_getter_with_string_primary_keys
@@ -869,6 +884,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [dev], company.developers
end
+ def test_collection_singular_ids_setter_with_required_type_cast
+ company = companies(:rails_core)
+ dev = Developer.first
+
+ company.developer_ids = [dev.id.to_s]
+ assert_equal [dev], company.developers
+ end
+
def test_collection_singular_ids_setter_with_string_primary_keys
assert_nothing_raised do
book = books(:awdr)
@@ -926,8 +949,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
def test_through_association_readonly_should_be_false
- assert !people(:michael).posts.first.readonly?
- assert !people(:michael).posts.to_a.first.readonly?
+ assert_not_predicate people(:michael).posts.first, :readonly?
+ assert_not_predicate people(:michael).posts.to_a.first, :readonly?
end
def test_can_update_through_association
@@ -1016,12 +1039,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post.author_categorizations
proxy = post.send(:association_instance_get, :author_categorizations)
- assert !proxy.stale_target?
+ assert_not_predicate proxy, :stale_target?
assert_equal authors(:mary).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id)
post.author_id = authors(:david).id
- assert proxy.stale_target?
+ assert_predicate proxy, :stale_target?
assert_equal authors(:david).categorizations.sort_by(&:id), post.author_categorizations.sort_by(&:id)
end
@@ -1038,7 +1061,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_includes post.author_addresses, address
post.author_addresses.delete(address)
- assert post[:author_count].nil?
+ assert_predicate post[:author_count], :nil?
end
def test_primary_key_option_on_source
@@ -1250,10 +1273,18 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
TenantMembership.current_member = nil
end
+ def test_has_many_through_with_scope_that_has_joined_same_table_with_parent_relation
+ assert_equal authors(:david), Author.joins(:comments_for_first_author).take
+ end
+
def test_has_many_through_with_unscope_should_affect_to_through_scope
assert_equal [comments(:eager_other_comment1)], authors(:mary).unordered_comments
end
+ def test_has_many_through_with_scope_should_accept_string_and_hash_join
+ assert_equal authors(:david), Author.joins({ comments_for_first_author: :post }, "inner join posts posts_alias on authors.id = posts_alias.author_id").eager_load(:categories).take
+ end
+
def test_has_many_through_with_scope_should_respect_table_alias
family = Family.create!
users = 3.times.map { User.create! }
@@ -1292,6 +1323,70 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_has_many_through_update_ids_with_conditions
+ author = Author.create!(name: "Bill")
+ category = categories(:general)
+
+ author.update(
+ special_categories_with_condition_ids: [category.id],
+ nonspecial_categories_with_condition_ids: [category.id]
+ )
+
+ assert_equal [category.id], author.special_categories_with_condition_ids
+ assert_equal [category.id], author.nonspecial_categories_with_condition_ids
+
+ author.update(nonspecial_categories_with_condition_ids: [])
+ author.reload
+
+ assert_equal [category.id], author.special_categories_with_condition_ids
+ assert_equal [], author.nonspecial_categories_with_condition_ids
+ end
+
+ def test_single_has_many_through_association_with_unpersisted_parent_instance
+ post_with_single_has_many_through = Class.new(Post) do
+ def self.name; "PostWithSingleHasManyThrough"; end
+ has_many :subscriptions, through: :author
+ end
+ post = post_with_single_has_many_through.new
+
+ post.author = authors(:mary)
+ book1 = Book.create!(name: "essays on single has many through associations 1")
+ post.author.books << book1
+ subscription1 = Subscription.first
+ book1.subscriptions << subscription1
+ assert_equal [subscription1], post.subscriptions.to_a
+
+ post.author = authors(:bob)
+ book2 = Book.create!(name: "essays on single has many through associations 2")
+ post.author.books << book2
+ subscription2 = Subscription.second
+ book2.subscriptions << subscription2
+ assert_equal [subscription2], post.subscriptions.to_a
+ end
+
+ def test_nested_has_many_through_association_with_unpersisted_parent_instance
+ post_with_nested_has_many_through = Class.new(Post) do
+ def self.name; "PostWithNestedHasManyThrough"; end
+ has_many :books, through: :author
+ has_many :subscriptions, through: :books
+ end
+ post = post_with_nested_has_many_through.new
+
+ post.author = authors(:mary)
+ book1 = Book.create!(name: "essays on nested has many through associations 1")
+ post.author.books << book1
+ subscription1 = Subscription.first
+ book1.subscriptions << subscription1
+ assert_equal [subscription1], post.subscriptions.to_a
+
+ post.author = authors(:bob)
+ book2 = Book.create!(name: "essays on nested has many through associations 2")
+ post.author.books << book2
+ subscription2 = Subscription.second
+ book2.subscriptions << subscription2
+ assert_equal [subscription2], post.subscriptions.to_a
+ 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 2a9ebd19ed..d7e898a1c0 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -30,7 +30,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
ActiveRecord::SQLCounter.clear_log
companies(:first_firm).account
ensure
- assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query"
+ log_all = ActiveRecord::SQLCounter.log_all
+ assert log_all.all? { |sql| /order by/i !~ sql }, "ORDER BY was used in the query: #{log_all}"
end
def test_has_one_cache_nils
@@ -113,8 +114,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
developer = Developer.create!(name: "Someone")
ship = Ship.create!(name: "Planet Caravan", developer: developer)
ship.destroy
- assert !ship.persisted?
- assert !developer.persisted?
+ assert_not_predicate ship, :persisted?
+ assert_not_predicate developer, :persisted?
end
def test_natural_assignment_to_nil_after_destroy
@@ -185,7 +186,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
assert RestrictedWithExceptionFirm.exists?(name: "restrict")
- assert firm.account.present?
+ assert_predicate firm.account, :present?
end
def test_restrict_with_error
@@ -196,10 +197,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
firm.destroy
- assert !firm.errors.empty?
+ assert_not_empty firm.errors
assert_equal "Cannot delete record because a dependent account exists", firm.errors[:base].first
assert RestrictedWithErrorFirm.exists?(name: "restrict")
- assert firm.account.present?
+ assert_predicate firm.account, :present?
end
def test_restrict_with_error_with_locale
@@ -212,10 +213,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
firm.destroy
- assert !firm.errors.empty?
+ assert_not_empty firm.errors
assert_equal "Cannot delete record because a dependent firm account exists", firm.errors[:base].first
assert RestrictedWithErrorFirm.exists?(name: "restrict")
- assert firm.account.present?
+ assert_predicate firm.account, :present?
ensure
I18n.backend.reload!
end
@@ -376,7 +377,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_assignment_before_child_saved
firm = Firm.find(1)
firm.account = a = Account.new("credit_limit" => 1000)
- assert a.persisted?
+ assert_predicate a, :persisted?
assert_equal a, firm.account
assert_equal a, firm.account
firm.association(:account).reload
@@ -394,7 +395,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_cant_save_readonly_association
assert_raise(ActiveRecord::ReadOnlyRecord) { companies(:first_firm).readonly_account.save! }
- assert companies(:first_firm).readonly_account.readonly?
+ assert_predicate companies(:first_firm).readonly_account, :readonly?
end
def test_has_one_proxy_should_not_respond_to_private_methods
@@ -432,7 +433,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
def test_create_respects_hash_condition
account = companies(:first_firm).create_account_limit_500_with_hash_conditions
- assert account.persisted?
+ assert_predicate account, :persisted?
assert_equal 500, account.credit_limit
end
@@ -449,9 +450,9 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
new_ship = pirate.create_ship
assert_not_equal ships(:black_pearl), new_ship
assert_equal new_ship, pirate.ship
- assert new_ship.new_record?
+ 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
@@ -459,8 +460,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
orig_ship = pirate.dependent_ship
new_ship = pirate.create_dependent_ship
- assert new_ship.new_record?
- assert orig_ship.destroyed?
+ assert_predicate new_ship, :new_record?
+ assert_predicate orig_ship, :destroyed?
end
def test_creation_failure_due_to_new_record_should_raise_error
@@ -480,7 +481,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
pirate = pirates(:blackbeard)
pirate.ship.name = nil
- assert !pirate.ship.valid?
+ assert_not_predicate pirate.ship, :valid?
error = assert_raise(ActiveRecord::RecordNotSaved) do
pirate.ship = ships(:interceptor)
end
@@ -587,7 +588,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
ship.save!
ship.name = "new name"
- assert ship.changed?
+ assert_predicate ship, :changed?
assert_queries(1) do
# One query for updating name, not triggering query for updating pirate_id
pirate.ship = ship
@@ -677,7 +678,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
book = SpecialBook.create!(status: "published")
author.book = book
- refute_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count
+ assert_not_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count
end
def test_association_enum_works_properly_with_nested_join
@@ -724,4 +725,28 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_not DestroyByParentBook.exists?(book.id)
end
+
+ class UndestroyableBook < ActiveRecord::Base
+ self.table_name = "books"
+ belongs_to :author, class_name: "DestroyableAuthor"
+ before_destroy :dont
+
+ def dont
+ throw(:abort)
+ end
+ end
+
+ class DestroyableAuthor < ActiveRecord::Base
+ self.table_name = "authors"
+ has_one :book, class_name: "UndestroyableBook", foreign_key: "author_id", dependent: :destroy
+ end
+
+ def test_dependency_should_halt_parent_destruction
+ author = DestroyableAuthor.create!(name: "Test")
+ UndestroyableBook.create!(author: author)
+
+ assert_no_difference ["DestroyableAuthor.count", "UndestroyableBook.count"] do
+ assert_not author.destroy
+ end
+ end
end
diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb
index fe24c465b2..0309663943 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -42,6 +42,18 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
assert_not_nil new_member.club
end
+ def test_creating_association_builds_through_record
+ new_member = Member.create(name: "Chris")
+ new_club = new_member.association(:club).build
+ assert new_member.current_membership
+ assert_equal new_club, new_member.club
+ assert_predicate new_club, :new_record?
+ assert_predicate new_member.current_membership, :new_record?
+ assert new_member.save
+ assert_predicate new_club, :persisted?
+ assert_predicate new_member.current_membership, :persisted?
+ end
+
def test_creating_association_builds_through_record_for_new
new_member = Member.new(name: "Jane")
new_member.club = clubs(:moustache_club)
@@ -52,6 +64,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")
@@ -100,7 +130,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_one_through_eager_loading
- members = assert_queries(3) do #base table, through table, clubs table
+ members = assert_queries(3) do # base table, through table, clubs table
Member.all.merge!(includes: :club, where: ["name = ?", "Groucho Marx"]).to_a
end
assert_equal 1, members.size
@@ -108,7 +138,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_one_through_eager_loading_through_polymorphic
- members = assert_queries(3) do #base table, through table, clubs table
+ members = assert_queries(3) do # base table, through table, clubs table
Member.all.merge!(includes: :sponsor_club, where: ["name = ?", "Groucho Marx"]).to_a
end
assert_equal 1, members.size
@@ -139,7 +169,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_nonpreload_eagerloading
members = assert_queries(1) do
- Member.all.merge!(includes: :club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a #force fallback
+ Member.all.merge!(includes: :club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a # force fallback
end
assert_equal 1, members.size
assert_not_nil assert_no_queries { members[0].club }
@@ -147,7 +177,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_nonpreload_eager_loading_through_polymorphic
members = assert_queries(1) do
- Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a #force fallback
+ Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name").to_a # force fallback
end
assert_equal 1, members.size
assert_not_nil assert_no_queries { members[0].sponsor_club }
@@ -156,7 +186,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record
Sponsor.new(sponsor_club: clubs(:crazy_club), sponsorable: members(:groucho)).save!
members = assert_queries(1) do
- Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name DESC").to_a #force fallback
+ Member.all.merge!(includes: :sponsor_club, where: ["members.name = ?", "Groucho Marx"], order: "clubs.name DESC").to_a # force fallback
end
assert_equal 1, members.size
assert_not_nil assert_no_queries { members[0].sponsor_club }
@@ -229,7 +259,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
MemberDetail.all.merge!(includes: :member_type).to_a
end
@new_detail = @member_details[0]
- assert @new_detail.send(:association, :member_type).loaded?
+ assert_predicate @new_detail.send(:association, :member_type), :loaded?
assert_no_queries { @new_detail.member_type }
end
@@ -317,12 +347,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
minivan.dashboard
proxy = minivan.send(:association_instance_get, :dashboard)
- assert !proxy.stale_target?
+ assert_not_predicate proxy, :stale_target?
assert_equal dashboards(:cool_first), minivan.dashboard
minivan.speedometer_id = speedometers(:second).id
- assert proxy.stale_target?
+ assert_predicate proxy, :stale_target?
assert_equal dashboards(:second), minivan.dashboard
end
@@ -334,7 +364,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
minivan.speedometer_id = speedometers(:second).id
- assert proxy.stale_target?
+ assert_predicate proxy, :stale_target?
assert_equal dashboards(:second), minivan.dashboard
end
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index 23be344419..ca0620dc3b 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -27,6 +27,24 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
end
end
+ 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)
+ end
+
+ def test_construct_finder_sql_does_not_table_name_collide_with_string_joins
+ sql = Person.joins(:agents).joins("JOIN people agents_people ON agents_people.primary_contact_id = people.id").to_sql
+ assert_match(/agents_people_2/i, sql)
+ end
+
+ def test_construct_finder_sql_does_not_table_name_collide_with_aliased_joins
+ people = Person.arel_table
+ agents = people.alias("agents_people")
+ constraint = agents[:primary_contact_id].eq(people[:id])
+ sql = Person.joins(:agents).joins(agents.create_join(agents, agents.create_on(constraint))).to_sql
+ assert_match(/agents_people_2/i, sql)
+ end
+
def test_construct_finder_sql_ignores_empty_joins_hash
sql = Author.joins({}).to_sql
assert_no_match(/JOIN/i, sql)
@@ -97,19 +115,19 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
scope = Post.joins(:special_comments).where(id: posts(:sti_comments).id)
# The join should match SpecialComment and its subclasses only
- assert scope.where("comments.type" => "Comment").empty?
- assert !scope.where("comments.type" => "SpecialComment").empty?
- assert !scope.where("comments.type" => "SubSpecialComment").empty?
+ assert_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
+ assert_not_empty scope.where("comments.type" => "SubSpecialComment")
end
def test_find_with_conditions_on_reflection
- assert !posts(:welcome).comments.empty?
+ assert_not_empty posts(:welcome).comments
assert Post.joins(:nonexistent_comments).where(id: posts(:welcome).id).empty? # [sic!]
end
def test_find_with_conditions_on_through_reflection
- assert !posts(:welcome).tags.empty?
- assert Post.joins(:misc_tags).where(id: posts(:welcome).id).empty?
+ assert_not_empty posts(:welcome).tags
+ assert_empty Post.joins(:misc_tags).where(id: posts(:welcome).id)
end
test "the default scope of the target is applied when joining associations" do
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index e13cf93dcf..896bf574f4 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -129,7 +129,7 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase
def test_polymorphic_has_one_should_find_inverse_automatically
man_reflection = Man.reflect_on_association(:polymorphic_face_without_inverse)
- assert man_reflection.has_inverse?
+ assert_predicate man_reflection, :has_inverse?
end
end
@@ -150,22 +150,22 @@ class InverseAssociationTests < ActiveRecord::TestCase
def test_should_be_able_to_ask_a_reflection_if_it_has_an_inverse
has_one_with_inverse_ref = Man.reflect_on_association(:face)
- assert has_one_with_inverse_ref.has_inverse?
+ assert_predicate has_one_with_inverse_ref, :has_inverse?
has_many_with_inverse_ref = Man.reflect_on_association(:interests)
- assert has_many_with_inverse_ref.has_inverse?
+ assert_predicate has_many_with_inverse_ref, :has_inverse?
belongs_to_with_inverse_ref = Face.reflect_on_association(:man)
- assert belongs_to_with_inverse_ref.has_inverse?
+ assert_predicate belongs_to_with_inverse_ref, :has_inverse?
has_one_without_inverse_ref = Club.reflect_on_association(:sponsor)
- assert !has_one_without_inverse_ref.has_inverse?
+ assert_not_predicate has_one_without_inverse_ref, :has_inverse?
has_many_without_inverse_ref = Club.reflect_on_association(:memberships)
- assert !has_many_without_inverse_ref.has_inverse?
+ assert_not_predicate has_many_without_inverse_ref, :has_inverse?
belongs_to_without_inverse_ref = Sponsor.reflect_on_association(:sponsor_club)
- assert !belongs_to_without_inverse_ref.has_inverse?
+ assert_not_predicate belongs_to_without_inverse_ref, :has_inverse?
end
def test_inverse_of_method_should_supply_the_actual_reflection_instance_it_is_the_inverse_of
@@ -190,6 +190,16 @@ class InverseAssociationTests < ActiveRecord::TestCase
assert_nil belongs_to_ref.inverse_of
end
+ def test_polymorphic_associations_dont_attempt_to_find_inverse_of
+ belongs_to_ref = Sponsor.reflect_on_association(:sponsor)
+ assert_raise(ArgumentError) { belongs_to_ref.klass }
+ assert_nil belongs_to_ref.inverse_of
+
+ belongs_to_ref = Face.reflect_on_association(:human)
+ assert_raise(ArgumentError) { belongs_to_ref.klass }
+ assert_nil belongs_to_ref.inverse_of
+ end
+
def test_this_inverse_stuff
firm = Firm.create!(name: "Adequate Holdings")
Project.create!(name: "Project 1", firm: firm)
@@ -464,7 +474,7 @@ class InverseHasManyTests < ActiveRecord::TestCase
interest = Interest.create!(man: man)
man.interests.find(interest.id)
- assert_not man.interests.loaded?
+ assert_not_predicate man.interests, :loaded?
end
def test_raise_record_not_found_error_when_invalid_ids_are_passed
@@ -484,7 +494,10 @@ class InverseHasManyTests < ActiveRecord::TestCase
def test_raise_record_not_found_error_when_no_ids_are_passed
man = Man.create!
- assert_raise(ActiveRecord::RecordNotFound) { man.interests.find() }
+ exception = assert_raise(ActiveRecord::RecordNotFound) { man.interests.load.find() }
+
+ assert_equal exception.model, "Interest"
+ assert_equal exception.primary_key, "id"
end
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
@@ -501,16 +514,16 @@ class InverseHasManyTests < ActiveRecord::TestCase
i.man.name = "Charles"
assert_equal i.man.name, man.name
- assert !man.persisted?
+ assert_not_predicate man, :persisted?
end
def test_inverse_instance_should_be_set_before_find_callbacks_are_run
reset_callbacks(Interest, :find) do
Interest.after_find { raise unless association(:man).loaded? && man.present? }
- assert Man.first.interests.reload.any?
- assert Man.includes(:interests).first.interests.any?
- assert Man.joins(:interests).includes(:interests).first.interests.any?
+ assert_predicate Man.first.interests.reload, :any?
+ assert_predicate Man.includes(:interests).first.interests, :any?
+ assert_predicate Man.joins(:interests).includes(:interests).first.interests, :any?
end
end
@@ -518,9 +531,9 @@ class InverseHasManyTests < ActiveRecord::TestCase
reset_callbacks(Interest, :initialize) do
Interest.after_initialize { raise unless association(:man).loaded? && man.present? }
- assert Man.first.interests.reload.any?
- assert Man.includes(:interests).first.interests.any?
- assert Man.joins(:interests).includes(:interests).first.interests.any?
+ assert_predicate Man.first.interests.reload, :any?
+ assert_predicate Man.includes(:interests).first.interests, :any?
+ assert_predicate Man.joins(:interests).includes(:interests).first.interests, :any?
end
end
@@ -672,6 +685,16 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
assert_equal old_inversed_man.object_id, new_inversed_man.object_id
end
+ def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed_with_validation
+ face = Face.new man: Man.new
+
+ old_inversed_man = face.man
+ face.save!
+ new_inversed_man = face.man
+
+ assert_equal old_inversed_man.object_id, new_inversed_man.object_id
+ end
+
def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
i = interests(:llama_wrangling)
m = i.polymorphic_man
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 87694b0788..9d1c73c33b 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -44,11 +44,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_has_many_distinct_through_count
author = authors(:mary)
- assert !authors(:mary).unique_categorized_posts.loaded?
+ assert_not_predicate authors(:mary).unique_categorized_posts, :loaded?
assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count }
assert_queries(1) { assert_equal 1, author.unique_categorized_posts.count(:title) }
assert_queries(1) { assert_equal 0, author.unique_categorized_posts.where(title: nil).count(:title) }
- assert !authors(:mary).unique_categorized_posts.loaded?
+ assert_not_predicate authors(:mary).unique_categorized_posts, :loaded?
end
def test_has_many_distinct_through_find
@@ -369,7 +369,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
Tag.has_many :null_taggings, -> { none }, class_name: :Tagging
Tag.has_many :null_tagged_posts, through: :null_taggings, source: "taggable", source_type: "Post"
assert_equal [], tags(:general).null_tagged_posts
- refute_equal [], tags(:general).tagged_posts
+ assert_not_equal [], tags(:general).tagged_posts
end
def test_eager_has_many_polymorphic_with_source_type
@@ -454,8 +454,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_has_many_through_uses_conditions_specified_on_the_has_many_association
author = Author.first
- assert author.comments.present?
- assert author.nonexistent_comments.blank?
+ assert_predicate author.comments, :present?
+ assert_predicate author.nonexistent_comments, :blank?
end
def test_has_many_through_uses_correct_attributes
@@ -467,27 +467,27 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
new_tag = Tag.new(name: "new")
saved_post.tags << new_tag
- assert new_tag.persisted? #consistent with habtm!
- assert saved_post.persisted?
+ assert new_tag.persisted? # consistent with habtm!
+ assert_predicate saved_post, :persisted?
assert_includes saved_post.tags, new_tag
- assert new_tag.persisted?
+ assert_predicate new_tag, :persisted?
assert_includes saved_post.reload.tags.reload, new_tag
new_post = Post.new(title: "Association replacement works!", body: "You best believe it.")
saved_tag = tags(:general)
new_post.tags << saved_tag
- assert !new_post.persisted?
- assert saved_tag.persisted?
+ assert_not_predicate new_post, :persisted?
+ assert_predicate saved_tag, :persisted?
assert_includes new_post.tags, saved_tag
new_post.save!
- assert new_post.persisted?
+ assert_predicate new_post, :persisted?
assert_includes new_post.reload.tags.reload, saved_tag
- assert !posts(:thinking).tags.build.persisted?
- assert !posts(:thinking).tags.new.persisted?
+ assert_not_predicate posts(:thinking).tags.build, :persisted?
+ assert_not_predicate posts(:thinking).tags.new, :persisted?
end
def test_create_associate_when_adding_to_has_many_through
@@ -529,14 +529,14 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_has_many_through_collection_size_doesnt_load_target_if_not_loaded
author = authors(:david)
assert_equal 10, author.comments.size
- assert !author.comments.loaded?
+ assert_not_predicate author.comments, :loaded?
end
def test_has_many_through_collection_size_uses_counter_cache_if_it_exists
c = categories(:general)
c.categorizations_count = 100
assert_equal 100, c.categorizations.size
- assert !c.categorizations.loaded?
+ assert_not_predicate c.categorizations, :loaded?
end
def test_adding_junk_to_has_many_through_should_raise_type_mismatch
@@ -710,7 +710,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
category = david.categories.first
assert_no_queries do
- assert david.categories.loaded?
+ assert_predicate david.categories, :loaded?
assert_includes david.categories, category
end
end
@@ -720,19 +720,19 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
category = david.categories.first
david.reload
- assert ! david.categories.loaded?
+ assert_not_predicate david.categories, :loaded?
assert_queries(1) do
assert_includes david.categories, category
end
- assert ! david.categories.loaded?
+ assert_not_predicate david.categories, :loaded?
end
def test_has_many_through_include_returns_false_for_non_matching_record_to_verify_scoping
david = authors(:david)
category = Category.create!(name: "Not Associated")
- assert ! david.categories.loaded?
- assert ! david.categories.include?(category)
+ assert_not_predicate david.categories, :loaded?
+ 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 c95d0425cd..0e54e8c1b0 100644
--- a/activerecord/test/cases/associations/left_outer_join_association_test.rb
+++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb
@@ -5,6 +5,7 @@ require "models/post"
require "models/comment"
require "models/author"
require "models/essay"
+require "models/category"
require "models/categorization"
require "models/person"
@@ -69,15 +70,15 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase
scope = Post.left_outer_joins(:special_comments).where(id: posts(:sti_comments).id)
# The join should match SpecialComment and its subclasses only
- assert scope.where("comments.type" => "Comment").empty?
- assert !scope.where("comments.type" => "SpecialComment").empty?
- assert !scope.where("comments.type" => "SubSpecialComment").empty?
+ assert_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
+ assert_not_empty scope.where("comments.type" => "SubSpecialComment")
end
def test_does_not_override_select
authors = Author.select("authors.name, #{%{(authors.author_address_id || ' ' || authors.author_address_extra_id) as addr_id}}").left_outer_joins(:posts)
- assert authors.any?
- assert authors.first.respond_to?(:addr_id)
+ assert_predicate authors, :any?
+ assert_respond_to authors.first, :addr_id
end
test "the default scope of the target is applied when joining associations" do
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index 3e37e512ca..03ed1c1d47 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -24,6 +24,11 @@ require "models/category"
require "models/categorization"
require "models/membership"
require "models/essay"
+require "models/hotel"
+require "models/department"
+require "models/chef"
+require "models/cake_designer"
+require "models/drink_designer"
class NestedThroughAssociationsTest < ActiveRecord::TestCase
fixtures :authors, :author_addresses, :books, :posts, :subscriptions, :subscribers, :tags, :taggings,
@@ -73,7 +78,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
# This ensures that the polymorphism of taggings is being observed correctly
authors = Author.joins(:tags).where("taggings.taggable_type" => "FakeModel")
- assert authors.empty?
+ assert_empty authors
end
# has_many through
@@ -172,7 +177,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
members = Member.joins(:organization_member_details).
where("member_details.id" => 9)
- assert members.empty?
+ assert_empty members
end
# has_many through
@@ -204,7 +209,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
members = Member.joins(:organization_member_details_2).
where("member_details.id" => 9)
- assert members.empty?
+ assert_empty members
end
# has_many through
@@ -420,9 +425,14 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
# Check the polymorphism of taggings is being observed correctly (in both joins)
authors = Author.joins(:similar_posts).where("taggings.taggable_type" => "FakeModel")
- assert authors.empty?
+ assert_empty authors
authors = Author.joins(:similar_posts).where("taggings_authors_join.taggable_type" => "FakeModel")
- assert authors.empty?
+ assert_empty authors
+ end
+
+ def test_nested_has_many_through_with_scope_on_polymorphic_reflection
+ authors = Author.joins(:ordered_posts).where("posts.id" => posts(:misc_by_bob).id)
+ assert_equal [authors(:mary), authors(:bob)], authors.distinct.sort_by(&:id)
end
def test_has_many_through_with_foreign_key_option_on_through_reflection
@@ -446,9 +456,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
# Ensure STI is respected in the join
scope = Post.joins(:special_comments_ratings).where(id: posts(:sti_comments).id)
- assert scope.where("comments.type" => "Comment").empty?
- assert !scope.where("comments.type" => "SpecialComment").empty?
- assert !scope.where("comments.type" => "SubSpecialComment").empty?
+ assert_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
+ assert_not_empty scope.where("comments.type" => "SubSpecialComment")
end
def test_has_many_through_with_sti_on_nested_through_reflection
@@ -456,8 +466,8 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
assert_equal [taggings(:special_comment_rating)], taggings
scope = Post.joins(:special_comments_ratings_taggings).where(id: posts(:sti_comments).id)
- assert scope.where("comments.type" => "Comment").empty?
- assert !scope.where("comments.type" => "SpecialComment").empty?
+ assert_empty scope.where("comments.type" => "Comment")
+ assert_not_empty scope.where("comments.type" => "SpecialComment")
end
def test_nested_has_many_through_writers_should_raise_error
@@ -507,7 +517,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_nested_has_many_through_with_conditions_on_through_associations_preload
- assert Author.where("tags.id" => 100).joins(:misc_post_first_blue_tags).empty?
+ assert_empty Author.where("tags.id" => 100).joins(:misc_post_first_blue_tags)
authors = assert_queries(3) { Author.includes(:misc_post_first_blue_tags).to_a.sort_by(&:id) }
blue = tags(:blue)
@@ -564,9 +574,40 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
c = Categorization.new
c.author = authors(:david)
c.post_taggings.to_a
- assert !c.post_taggings.empty?
+ assert_not_empty c.post_taggings
c.save
- assert !c.post_taggings.empty?
+ assert_not_empty c.post_taggings
+ end
+
+ def test_polymorphic_has_many_through_when_through_association_has_not_loaded
+ cake_designer = CakeDesigner.create!(chef: Chef.new)
+ drink_designer = DrinkDesigner.create!(chef: Chef.new)
+ department = Department.create!(chefs: [cake_designer.chef, drink_designer.chef])
+ Hotel.create!(departments: [department])
+ hotel = Hotel.includes(:cake_designers, :drink_designers).take
+
+ assert_equal [cake_designer], hotel.cake_designers
+ assert_equal [drink_designer], hotel.drink_designers
+ end
+
+ def test_polymorphic_has_many_through_when_through_association_has_already_loaded
+ cake_designer = CakeDesigner.create!(chef: Chef.new)
+ drink_designer = DrinkDesigner.create!(chef: Chef.new)
+ department = Department.create!(chefs: [cake_designer.chef, drink_designer.chef])
+ Hotel.create!(departments: [department])
+ hotel = Hotel.includes(:chefs, :cake_designers, :drink_designers).take
+
+ assert_equal [cake_designer], hotel.cake_designers
+ assert_equal [drink_designer], hotel.drink_designers
+ end
+
+ def test_polymorphic_has_many_through_joined_different_table_twice
+ cake_designer = CakeDesigner.create!(chef: Chef.new)
+ drink_designer = DrinkDesigner.create!(chef: Chef.new)
+ department = Department.create!(chefs: [cake_designer.chef, drink_designer.chef])
+ hotel = Hotel.create!(departments: [department])
+
+ assert_equal hotel, Hotel.joins(:cake_designers, :drink_designers).take
end
private
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index de04221ea6..fce2a7eed1 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -47,7 +47,7 @@ class AssociationsTest < ActiveRecord::TestCase
ship = Ship.create!(name: "The good ship Dollypop")
part = ship.parts.create!(name: "Mast")
part.mark_for_destruction
- assert ship.parts[0].marked_for_destruction?
+ assert_predicate ship.parts[0], :marked_for_destruction?
end
def test_loading_the_association_target_should_load_most_recent_attributes_for_child_records_marked_for_destruction
@@ -119,7 +119,7 @@ class AssociationProxyTest < ActiveRecord::TestCase
david = authors(:david)
david.posts << (post = Post.new(title: "New on Edge", body: "More cool stuff!"))
- assert !david.posts.loaded?
+ assert_not_predicate david.posts, :loaded?
assert_includes david.posts, post
end
@@ -127,7 +127,7 @@ class AssociationProxyTest < ActiveRecord::TestCase
david = authors(:david)
david.categories << categories(:technology)
- assert !david.categories.loaded?
+ assert_not_predicate david.categories, :loaded?
assert_includes david.categories, categories(:technology)
end
@@ -135,23 +135,23 @@ class AssociationProxyTest < ActiveRecord::TestCase
david = authors(:david)
david.posts << (post = Post.new(title: "New on Edge", body: "More cool stuff!"))
- assert !david.posts.loaded?
+ assert_not_predicate david.posts, :loaded?
david.save
- assert !david.posts.loaded?
+ assert_not_predicate david.posts, :loaded?
assert_includes david.posts, post
end
def test_push_does_not_lose_additions_to_new_record
josh = Author.new(name: "Josh")
josh.posts << Post.new(title: "New on Edge", body: "More cool stuff!")
- assert josh.posts.loaded?
+ assert_predicate josh.posts, :loaded?
assert_equal 1, josh.posts.size
end
def test_append_behaves_like_push
josh = Author.new(name: "Josh")
josh.posts.append Post.new(title: "New on Edge", body: "More cool stuff!")
- assert josh.posts.loaded?
+ assert_predicate josh.posts, :loaded?
assert_equal 1, josh.posts.size
end
@@ -163,22 +163,22 @@ class AssociationProxyTest < ActiveRecord::TestCase
def test_save_on_parent_does_not_load_target
david = developers(:david)
- assert !david.projects.loaded?
+ assert_not_predicate david.projects, :loaded?
david.update_columns(created_at: Time.now)
- assert !david.projects.loaded?
+ assert_not_predicate david.projects, :loaded?
end
def test_load_does_load_target
david = developers(:david)
- assert !david.projects.loaded?
+ assert_not_predicate david.projects, :loaded?
david.projects.load
- assert david.projects.loaded?
+ assert_predicate david.projects, :loaded?
end
def test_inspect_does_not_reload_a_not_yet_loaded_target
andreas = Developer.new name: "Andreas", log: "new developer added"
- assert !andreas.audit_logs.loaded?
+ assert_not_predicate andreas.audit_logs, :loaded?
assert_match(/message: "new developer added"/, andreas.audit_logs.inspect)
end
@@ -248,14 +248,14 @@ class AssociationProxyTest < ActiveRecord::TestCase
test "first! works on loaded associations" do
david = authors(:david)
assert_equal david.first_posts.first, david.first_posts.reload.first!
- assert david.first_posts.loaded?
+ assert_predicate david.first_posts, :loaded?
assert_no_queries { david.first_posts.first! }
end
def test_pluck_uses_loaded_target
david = authors(:david)
assert_equal david.first_posts.pluck(:title), david.first_posts.load.pluck(:title)
- assert david.first_posts.loaded?
+ assert_predicate david.first_posts, :loaded?
assert_no_queries { david.first_posts.pluck(:title) }
end
@@ -263,9 +263,9 @@ class AssociationProxyTest < ActiveRecord::TestCase
david = authors(:david)
david.posts.reload
- assert david.posts.loaded?
+ assert_predicate david.posts, :loaded?
david.posts.reset
- assert !david.posts.loaded?
+ assert_not_predicate david.posts, :loaded?
end
end
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index 0170a6e98d..54512068ee 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -12,7 +12,7 @@ module ActiveRecord
def setup
@klass = Class.new(Class.new { def self.initialize_generated_modules; end }) do
def self.superclass; Base; end
- def self.base_class; self; end
+ def self.base_class?; true; end
def self.decorate_matching_attribute_types(*); end
include ActiveRecord::DefineCallbacks
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index 0ea8ef5cea..1a37ad963f 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -63,8 +63,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 +77,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
@@ -99,8 +99,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
test "boolean attributes" do
- assert !Topic.find(1).approved?
- assert Topic.find(2).approved?
+ assert_not_predicate Topic.find(1), :approved?
+ assert_predicate Topic.find(2), :approved?
end
test "set attributes" do
@@ -142,16 +142,16 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_respond_to topic, :title=
assert_respond_to topic, "author_name"
assert_respond_to topic, "attribute_names"
- assert !topic.respond_to?("nothingness")
- assert !topic.respond_to?(:nothingness)
+ assert_not_respond_to topic, "nothingness"
+ assert_not_respond_to topic, :nothingness
end
test "respond_to? with a custom primary key" do
keyboard = Keyboard.create
assert_not_nil keyboard.key_number
assert_equal keyboard.key_number, keyboard.id
- assert keyboard.respond_to?("key_number")
- assert keyboard.respond_to?("id")
+ assert_respond_to keyboard, "key_number"
+ assert_respond_to keyboard, "id"
end
test "id_before_type_cast with a custom primary key" do
@@ -170,8 +170,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
topic = klass.allocate
- assert !topic.respond_to?("nothingness")
- assert !topic.respond_to?(:nothingness)
+ assert_not_respond_to topic, "nothingness"
+ assert_not_respond_to topic, :nothingness
assert_respond_to topic, "title"
assert_respond_to topic, :title
end
@@ -200,12 +200,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
if current_adapter?(:Mysql2Adapter)
test "read attributes_before_type_cast on a boolean" do
bool = Boolean.create!("value" => false)
- if RUBY_PLATFORM.include?("java")
- # JRuby will return the value before typecast as string.
- assert_equal "0", bool.reload.attributes_before_type_cast["value"]
- else
- assert_equal 0, bool.reload.attributes_before_type_cast["value"]
- end
+ assert_equal 0, bool.reload.attributes_before_type_cast["value"]
end
end
@@ -462,30 +457,30 @@ class AttributeMethodsTest < ActiveRecord::TestCase
SQL
assert_equal "Firm", object.string_value
- assert object.string_value?
+ assert_predicate object, :string_value?
object.string_value = " "
- assert !object.string_value?
+ assert_not_predicate object, :string_value?
assert_equal 1, object.int_value.to_i
- assert object.int_value?
+ assert_predicate object, :int_value?
object.int_value = "0"
- assert !object.int_value?
+ assert_not_predicate object, :int_value?
end
test "non-attribute read and write" do
topic = Topic.new
- assert !topic.respond_to?("mumbo")
+ assert_not_respond_to topic, "mumbo"
assert_raise(NoMethodError) { topic.mumbo }
assert_raise(NoMethodError) { topic.mumbo = 5 }
end
test "undeclared attribute method does not affect respond_to? and method_missing" do
topic = @target.new(title: "Budget")
- assert topic.respond_to?("title")
+ assert_respond_to topic, "title"
assert_equal "Budget", topic.title
- assert !topic.respond_to?("title_hello_world")
+ assert_not_respond_to topic, "title_hello_world"
assert_raise(NoMethodError) { topic.title_hello_world }
end
@@ -496,7 +491,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
@target.attribute_method_prefix prefix
meth = "#{prefix}title"
- assert topic.respond_to?(meth)
+ assert_respond_to topic, meth
assert_equal ["title"], topic.send(meth)
assert_equal ["title", "a"], topic.send(meth, "a")
assert_equal ["title", 1, 2, 3], topic.send(meth, 1, 2, 3)
@@ -510,7 +505,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
topic = @target.new(title: "Budget")
meth = "title#{suffix}"
- assert topic.respond_to?(meth)
+ assert_respond_to topic, meth
assert_equal ["title"], topic.send(meth)
assert_equal ["title", "a"], topic.send(meth, "a")
assert_equal ["title", 1, 2, 3], topic.send(meth, 1, 2, 3)
@@ -524,7 +519,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
topic = @target.new(title: "Budget")
meth = "#{prefix}title#{suffix}"
- assert topic.respond_to?(meth)
+ assert_respond_to topic, meth
assert_equal ["title"], topic.send(meth)
assert_equal ["title", "a"], topic.send(meth, "a")
assert_equal ["title", 1, 2, 3], topic.send(meth, 1, 2, 3)
@@ -546,7 +541,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
else
topic = Topic.all.merge!(select: "topics.*, 1=2 as is_test").first
end
- assert !topic.is_test?
+ assert_not_predicate topic, :is_test?
end
test "typecast attribute from select to true" do
@@ -557,7 +552,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
else
topic = Topic.all.merge!(select: "topics.*, 2=2 as is_test").first
end
- assert topic.is_test?
+ assert_predicate topic, :is_test?
end
test "raises ActiveRecord::DangerousAttributeError when defining an AR method in a model" do
@@ -748,7 +743,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
expected_time = Time.utc(2000, 01, 01, 10)
assert_equal expected_time, record.bonus_time
- assert record.bonus_time.utc?
+ assert_predicate record.bonus_time, :utc?
end
end
end
@@ -772,7 +767,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
privatize("title")
topic = @target.new(title: "The pros and cons of programming naked.")
- assert !topic.respond_to?(:title)
+ assert_not_respond_to topic, :title
exception = assert_raise(NoMethodError) { topic.title }
assert_includes exception.message, "private method"
assert_equal "I'm private", topic.send(:title)
@@ -782,7 +777,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
privatize("title=(value)")
topic = @target.new
- assert !topic.respond_to?(:title=)
+ assert_not_respond_to topic, :title=
exception = assert_raise(NoMethodError) { topic.title = "Pants" }
assert_includes exception.message, "private method"
topic.send(:title=, "Very large pants")
@@ -792,7 +787,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
privatize("title?")
topic = @target.new(title: "Isaac Newton's pants")
- assert !topic.respond_to?(:title?)
+ assert_not_respond_to topic, :title?
exception = assert_raise(NoMethodError) { topic.title? }
assert_includes exception.message, "private method"
assert topic.send(:title?)
@@ -832,7 +827,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
@@ -846,8 +841,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
@@ -984,9 +979,9 @@ class AttributeMethodsTest < ActiveRecord::TestCase
test "came_from_user?" do
model = @target.first
- assert_not model.id_came_from_user?
+ assert_not_predicate model, :id_came_from_user?
model.id = "omg"
- assert model.id_came_from_user?
+ assert_predicate model, :id_came_from_user?
end
test "accessed_fields" do
@@ -999,6 +994,11 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal ["title"], model.accessed_fields
end
+ test "generated attribute methods ancestors have correct class" do
+ mod = Topic.send(:generated_attribute_methods)
+ assert_match %r(GeneratedAttributeMethods), mod.inspect
+ end
+
private
def new_topic_like_ar_class(&block)
@@ -1019,14 +1019,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase
ActiveRecord::Base.time_zone_aware_types = old_types
end
- def cached_columns
- Topic.columns.map(&:name)
- end
-
- def time_related_columns_on_topic
- Topic.columns.select { |c| [:time, :date, :datetime, :timestamp].include?(c.type) }
- end
-
def privatize(method_signature)
@target.class_eval(<<-private_method, __FILE__, __LINE__ + 1)
private
diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb
deleted file mode 100644
index 8be77ed88f..0000000000
--- a/activerecord/test/cases/attribute_set_test.rb
+++ /dev/null
@@ -1,255 +0,0 @@
-# frozen_string_literal: true
-
-require "cases/helper"
-
-module ActiveRecord
- class AttributeSetTest < ActiveRecord::TestCase
- test "building a new set from raw attributes" do
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
- attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
-
- assert_equal 1, attributes[:foo].value
- assert_equal 2.2, attributes[:bar].value
- assert_equal :foo, attributes[:foo].name
- assert_equal :bar, attributes[:bar].name
- end
-
- test "building with custom types" do
- builder = AttributeSet::Builder.new(foo: Type::Float.new)
- attributes = builder.build_from_database({ foo: "3.3", bar: "4.4" }, { bar: Type::Integer.new })
-
- assert_equal 3.3, attributes[:foo].value
- assert_equal 4, attributes[:bar].value
- end
-
- test "[] returns a null object" do
- builder = AttributeSet::Builder.new(foo: Type::Float.new)
- attributes = builder.build_from_database(foo: "3.3")
-
- assert_equal "3.3", attributes[:foo].value_before_type_cast
- assert_nil attributes[:bar].value_before_type_cast
- assert_equal :bar, attributes[:bar].name
- end
-
- test "duping creates a new hash, but does not dup the attributes" do
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
- attributes = builder.build_from_database(foo: 1, bar: "foo")
-
- # Ensure the type cast value is cached
- attributes[:foo].value
- attributes[:bar].value
-
- duped = attributes.dup
- duped.write_from_database(:foo, 2)
- duped[:bar].value << "bar"
-
- assert_equal 1, attributes[:foo].value
- assert_equal 2, duped[:foo].value
- assert_equal "foobar", attributes[:bar].value
- assert_equal "foobar", duped[:bar].value
- end
-
- test "deep_duping creates a new hash and dups each attribute" do
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
- attributes = builder.build_from_database(foo: 1, bar: "foo")
-
- # Ensure the type cast value is cached
- attributes[:foo].value
- attributes[:bar].value
-
- duped = attributes.deep_dup
- duped.write_from_database(:foo, 2)
- duped[:bar].value << "bar"
-
- assert_equal 1, attributes[:foo].value
- assert_equal 2, duped[:foo].value
- assert_equal "foo", attributes[:bar].value
- assert_equal "foobar", duped[:bar].value
- end
-
- test "freezing cloned set does not freeze original" do
- attributes = AttributeSet.new({})
- clone = attributes.clone
-
- clone.freeze
-
- assert clone.frozen?
- assert_not attributes.frozen?
- end
-
- test "to_hash returns a hash of the type cast values" do
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
- attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
-
- assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash)
- assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h)
- end
-
- test "to_hash maintains order" do
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
- attributes = builder.build_from_database(foo: "2.2", bar: "3.3")
-
- attributes[:bar]
- hash = attributes.to_h
-
- assert_equal [[:foo, 2], [:bar, 3.3]], hash.to_a
- end
-
- test "values_before_type_cast" do
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
- attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
-
- assert_equal({ foo: "1.1", bar: "2.2" }, attributes.values_before_type_cast)
- end
-
- test "known columns are built with uninitialized attributes" do
- attributes = attributes_with_uninitialized_key
- assert attributes[:foo].initialized?
- assert_not attributes[:bar].initialized?
- end
-
- test "uninitialized attributes are not included in the attributes hash" do
- attributes = attributes_with_uninitialized_key
- assert_equal({ foo: 1 }, attributes.to_hash)
- end
-
- test "uninitialized attributes are not included in keys" do
- attributes = attributes_with_uninitialized_key
- assert_equal [:foo], attributes.keys
- end
-
- test "uninitialized attributes return false for key?" do
- attributes = attributes_with_uninitialized_key
- assert attributes.key?(:foo)
- assert_not attributes.key?(:bar)
- end
-
- test "unknown attributes return false for key?" do
- attributes = attributes_with_uninitialized_key
- assert_not attributes.key?(:wibble)
- end
-
- test "fetch_value returns the value for the given initialized attribute" do
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
- attributes = builder.build_from_database(foo: "1.1", bar: "2.2")
-
- assert_equal 1, attributes.fetch_value(:foo)
- assert_equal 2.2, attributes.fetch_value(:bar)
- end
-
- test "fetch_value returns nil for unknown attributes" do
- attributes = attributes_with_uninitialized_key
- assert_nil attributes.fetch_value(:wibble) { "hello" }
- end
-
- test "fetch_value returns nil for unknown attributes when types has a default" do
- types = Hash.new(Type::Value.new)
- builder = AttributeSet::Builder.new(types)
- attributes = builder.build_from_database
-
- assert_nil attributes.fetch_value(:wibble) { "hello" }
- end
-
- test "fetch_value uses the given block for uninitialized attributes" do
- attributes = attributes_with_uninitialized_key
- value = attributes.fetch_value(:bar) { |n| n.to_s + "!" }
- assert_equal "bar!", value
- end
-
- test "fetch_value returns nil for uninitialized attributes if no block is given" do
- attributes = attributes_with_uninitialized_key
- assert_nil attributes.fetch_value(:bar)
- end
-
- test "the primary_key is always initialized" do
- builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, :foo)
- attributes = builder.build_from_database
-
- assert attributes.key?(:foo)
- assert_equal [:foo], attributes.keys
- assert attributes[:foo].initialized?
- end
-
- class MyType
- def cast(value)
- return if value.nil?
- value + " from user"
- end
-
- def deserialize(value)
- return if value.nil?
- value + " from database"
- end
-
- def assert_valid_value(*)
- end
- end
-
- test "write_from_database sets the attribute with database typecasting" do
- builder = AttributeSet::Builder.new(foo: MyType.new)
- attributes = builder.build_from_database
-
- assert_nil attributes.fetch_value(:foo)
-
- attributes.write_from_database(:foo, "value")
-
- assert_equal "value from database", attributes.fetch_value(:foo)
- end
-
- test "write_from_user sets the attribute with user typecasting" do
- builder = AttributeSet::Builder.new(foo: MyType.new)
- attributes = builder.build_from_database
-
- assert_nil attributes.fetch_value(:foo)
-
- attributes.write_from_user(:foo, "value")
-
- assert_equal "value from user", attributes.fetch_value(:foo)
- end
-
- def attributes_with_uninitialized_key
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new)
- builder.build_from_database(foo: "1.1")
- end
-
- test "freezing doesn't prevent the set from materializing" do
- builder = AttributeSet::Builder.new(foo: Type::String.new)
- attributes = builder.build_from_database(foo: "1")
-
- attributes.freeze
- assert_equal({ foo: "1" }, attributes.to_hash)
- end
-
- test "#accessed_attributes returns only attributes which have been read" do
- builder = AttributeSet::Builder.new(foo: Type::Value.new, bar: Type::Value.new)
- attributes = builder.build_from_database(foo: "1", bar: "2")
-
- assert_equal [], attributes.accessed
-
- attributes.fetch_value(:foo)
-
- assert_equal [:foo], attributes.accessed
- end
-
- test "#map returns a new attribute set with the changes applied" do
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
- attributes = builder.build_from_database(foo: "1", bar: "2")
- new_attributes = attributes.map do |attr|
- attr.with_cast_value(attr.value + 1)
- end
-
- assert_equal 2, new_attributes.fetch_value(:foo)
- assert_equal 3, new_attributes.fetch_value(:bar)
- end
-
- test "comparison for equality is correctly implemented" do
- builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new)
- attributes = builder.build_from_database(foo: "1", bar: "2")
- attributes2 = builder.build_from_database(foo: "1", bar: "2")
- attributes3 = builder.build_from_database(foo: "2", bar: "2")
-
- assert_equal attributes, attributes2
- assert_not_equal attributes2, attributes3
- end
- end
-end
diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb
deleted file mode 100644
index 1731e7926e..0000000000
--- a/activerecord/test/cases/attribute_test.rb
+++ /dev/null
@@ -1,255 +0,0 @@
-# frozen_string_literal: true
-
-require "cases/helper"
-
-module ActiveRecord
- class AttributeTest < ActiveRecord::TestCase
- setup do
- @type = Minitest::Mock.new
- end
-
- teardown do
- assert @type.verify
- end
-
- test "from_database + read type casts from database" do
- @type.expect(:deserialize, "type cast from database", ["a value"])
- attribute = Attribute.from_database(nil, "a value", @type)
-
- type_cast_value = attribute.value
-
- assert_equal "type cast from database", type_cast_value
- end
-
- test "from_user + read type casts from user" do
- @type.expect(:cast, "type cast from user", ["a value"])
- attribute = Attribute.from_user(nil, "a value", @type)
-
- type_cast_value = attribute.value
-
- assert_equal "type cast from user", type_cast_value
- end
-
- test "reading memoizes the value" do
- @type.expect(:deserialize, "from the database", ["whatever"])
- attribute = Attribute.from_database(nil, "whatever", @type)
-
- type_cast_value = attribute.value
- second_read = attribute.value
-
- assert_equal "from the database", type_cast_value
- assert_same type_cast_value, second_read
- end
-
- test "reading memoizes falsy values" do
- @type.expect(:deserialize, false, ["whatever"])
- attribute = Attribute.from_database(nil, "whatever", @type)
-
- attribute.value
- attribute.value
- end
-
- test "read_before_typecast returns the given value" do
- attribute = Attribute.from_database(nil, "raw value", @type)
-
- raw_value = attribute.value_before_type_cast
-
- assert_equal "raw value", raw_value
- end
-
- test "from_database + read_for_database type casts to and from database" do
- @type.expect(:deserialize, "read from database", ["whatever"])
- @type.expect(:serialize, "ready for database", ["read from database"])
- attribute = Attribute.from_database(nil, "whatever", @type)
-
- serialize = attribute.value_for_database
-
- assert_equal "ready for database", serialize
- end
-
- test "from_user + read_for_database type casts from the user to the database" do
- @type.expect(:cast, "read from user", ["whatever"])
- @type.expect(:serialize, "ready for database", ["read from user"])
- attribute = Attribute.from_user(nil, "whatever", @type)
-
- serialize = attribute.value_for_database
-
- assert_equal "ready for database", serialize
- end
-
- test "duping dups the value" do
- @type.expect(:deserialize, "type cast".dup, ["a value"])
- attribute = Attribute.from_database(nil, "a value", @type)
-
- value_from_orig = attribute.value
- value_from_clone = attribute.dup.value
- value_from_orig << " foo"
-
- assert_equal "type cast foo", value_from_orig
- assert_equal "type cast", value_from_clone
- end
-
- test "duping does not dup the value if it is not dupable" do
- @type.expect(:deserialize, false, ["a value"])
- attribute = Attribute.from_database(nil, "a value", @type)
-
- assert_same attribute.value, attribute.dup.value
- end
-
- test "duping does not eagerly type cast if we have not yet type cast" do
- attribute = Attribute.from_database(nil, "a value", @type)
- attribute.dup
- end
-
- class MyType
- def cast(value)
- value + " from user"
- end
-
- def deserialize(value)
- value + " from database"
- end
-
- def assert_valid_value(*)
- end
- end
-
- test "with_value_from_user returns a new attribute with the value from the user" do
- old = Attribute.from_database(nil, "old", MyType.new)
- new = old.with_value_from_user("new")
-
- assert_equal "old from database", old.value
- assert_equal "new from user", new.value
- end
-
- test "with_value_from_database returns a new attribute with the value from the database" do
- old = Attribute.from_user(nil, "old", MyType.new)
- new = old.with_value_from_database("new")
-
- assert_equal "old from user", old.value
- assert_equal "new from database", new.value
- end
-
- test "uninitialized attributes yield their name if a block is given to value" do
- block = proc { |name| name.to_s + "!" }
- foo = Attribute.uninitialized(:foo, nil)
- bar = Attribute.uninitialized(:bar, nil)
-
- assert_equal "foo!", foo.value(&block)
- assert_equal "bar!", bar.value(&block)
- end
-
- test "uninitialized attributes have no value" do
- assert_nil Attribute.uninitialized(:foo, nil).value
- end
-
- test "attributes equal other attributes with the same constructor arguments" do
- first = Attribute.from_database(:foo, 1, Type::Integer.new)
- second = Attribute.from_database(:foo, 1, Type::Integer.new)
- assert_equal first, second
- end
-
- test "attributes do not equal attributes with different names" do
- first = Attribute.from_database(:foo, 1, Type::Integer.new)
- second = Attribute.from_database(:bar, 1, Type::Integer.new)
- assert_not_equal first, second
- end
-
- test "attributes do not equal attributes with different types" do
- first = Attribute.from_database(:foo, 1, Type::Integer.new)
- second = Attribute.from_database(:foo, 1, Type::Float.new)
- assert_not_equal first, second
- end
-
- test "attributes do not equal attributes with different values" do
- first = Attribute.from_database(:foo, 1, Type::Integer.new)
- second = Attribute.from_database(:foo, 2, Type::Integer.new)
- assert_not_equal first, second
- end
-
- test "attributes do not equal attributes of other classes" do
- first = Attribute.from_database(:foo, 1, Type::Integer.new)
- second = Attribute.from_user(:foo, 1, Type::Integer.new)
- assert_not_equal first, second
- end
-
- test "an attribute has not been read by default" do
- attribute = Attribute.from_database(:foo, 1, Type::Value.new)
- assert_not attribute.has_been_read?
- end
-
- test "an attribute has been read when its value is calculated" do
- attribute = Attribute.from_database(:foo, 1, Type::Value.new)
- attribute.value
- assert attribute.has_been_read?
- end
-
- test "an attribute is not changed if it hasn't been assigned or mutated" do
- attribute = Attribute.from_database(:foo, 1, Type::Value.new)
-
- refute attribute.changed?
- end
-
- test "an attribute is changed if it's been assigned a new value" do
- attribute = Attribute.from_database(:foo, 1, Type::Value.new)
- changed = attribute.with_value_from_user(2)
-
- assert changed.changed?
- end
-
- test "an attribute is not changed if it's assigned the same value" do
- attribute = Attribute.from_database(:foo, 1, Type::Value.new)
- unchanged = attribute.with_value_from_user(1)
-
- refute unchanged.changed?
- end
-
- test "an attribute can not be mutated if it has not been read,
- and skips expensive calculations" do
- type_which_raises_from_all_methods = Object.new
- attribute = Attribute.from_database(:foo, "bar", type_which_raises_from_all_methods)
-
- assert_not attribute.changed_in_place?
- end
-
- test "an attribute is changed if it has been mutated" do
- attribute = Attribute.from_database(:foo, "bar", Type::String.new)
- attribute.value << "!"
-
- assert attribute.changed_in_place?
- assert attribute.changed?
- end
-
- test "an attribute can forget its changes" do
- attribute = Attribute.from_database(:foo, "bar", Type::String.new)
- changed = attribute.with_value_from_user("foo")
- forgotten = changed.forgetting_assignment
-
- assert changed.changed? # sanity check
- refute forgotten.changed?
- end
-
- test "with_value_from_user validates the value" do
- type = Type::Value.new
- type.define_singleton_method(:assert_valid_value) do |value|
- if value == 1
- raise ArgumentError
- end
- end
-
- attribute = Attribute.from_database(:foo, 1, type)
- assert_equal 1, attribute.value
- assert_equal 2, attribute.with_value_from_user(2).value
- assert_raises ArgumentError do
- attribute.with_value_from_user(1)
- end
- end
-
- test "with_type preserves mutations" do
- attribute = Attribute.from_database(:foo, "".dup, Type::Value.new)
- attribute.value << "1"
-
- assert_equal 1, attribute.with_type(Type::Integer.new).value
- end
- end
-end
diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb
index 2caf2a63d4..3bc56694be 100644
--- a/activerecord/test/cases/attributes_test.rb
+++ b/activerecord/test/cases/attributes_test.rb
@@ -59,7 +59,7 @@ module ActiveRecord
test "nonexistent attribute" do
data = OverloadedType.new(non_existent_decimal: 1)
- assert_equal BigDecimal.new(1), data.non_existent_decimal
+ assert_equal BigDecimal(1), data.non_existent_decimal
assert_raise ActiveRecord::UnknownAttributeError do
UnoverloadedType.new(non_existent_decimal: 1)
end
@@ -211,7 +211,7 @@ module ActiveRecord
end
test "attributes not backed by database columns are not dirty when unchanged" do
- refute OverloadedType.new.non_existent_decimal_changed?
+ assert_not_predicate OverloadedType.new, :non_existent_decimal_changed?
end
test "attributes not backed by database columns are always initialized" do
@@ -245,13 +245,13 @@ module ActiveRecord
model.foo << "asdf"
assert_equal "lolasdf", model.foo
- assert model.foo_changed?
+ assert_predicate model, :foo_changed?
model.reload
assert_equal "lol", model.foo
model.foo = "lol"
- refute model.changed?
+ assert_not_predicate model, :changed?
end
test "attributes not backed by database columns appear in inspect" do
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 4d8368fd8a..0c212ecc22 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -50,8 +50,8 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
}
u = person.create!(first_name: "cool")
- u.update_attributes!(first_name: "nah") # still valid because validation only applies on 'create'
- assert reference.create!(person: u).persisted?
+ u.update!(first_name: "nah") # still valid because validation only applies on 'create'
+ assert_predicate reference.create!(person: u), :persisted?
end
def test_should_not_add_the_same_callbacks_multiple_times_for_has_one
@@ -74,7 +74,7 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase
ship = ShipWithoutNestedAttributes.new
ship.prisoners.build
- assert_not ship.valid?
+ assert_not_predicate ship, :valid?
assert_equal 1, ship.errors[:name].length
end
@@ -99,35 +99,35 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
def test_should_save_parent_but_not_invalid_child
firm = Firm.new(name: "GlobalMegaCorp")
- assert firm.valid?
+ assert_predicate firm, :valid?
firm.build_account_using_primary_key
- assert !firm.build_account_using_primary_key.valid?
+ assert_not_predicate firm.build_account_using_primary_key, :valid?
assert firm.save
- assert !firm.account_using_primary_key.persisted?
+ assert_not_predicate firm.account_using_primary_key, :persisted?
end
def test_save_fails_for_invalid_has_one
firm = Firm.first
- assert firm.valid?
+ assert_predicate firm, :valid?
firm.build_account
- assert !firm.account.valid?
- assert !firm.valid?
- assert !firm.save
+ assert_not_predicate firm.account, :valid?
+ assert_not_predicate firm, :valid?
+ assert_not firm.save
assert_equal ["is invalid"], firm.errors["account"]
end
def test_save_succeeds_for_invalid_has_one_with_validate_false
firm = Firm.first
- assert firm.valid?
+ assert_predicate firm, :valid?
firm.build_unvalidated_account
- assert !firm.unvalidated_account.valid?
- assert firm.valid?
+ assert_not_predicate firm.unvalidated_account, :valid?
+ assert_predicate firm, :valid?
assert firm.save
end
@@ -136,10 +136,10 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
account = firm.build_account("credit_limit" => 1000)
assert_equal account, firm.account
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
assert firm.save
assert_equal account, firm.account
- assert account.persisted?
+ assert_predicate account, :persisted?
end
def test_build_before_either_saved
@@ -147,16 +147,16 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
firm.account = account = Account.new("credit_limit" => 1000)
assert_equal account, firm.account
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
assert firm.save
assert_equal account, firm.account
- assert account.persisted?
+ assert_predicate account, :persisted?
end
def test_assignment_before_parent_saved
firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = a = Account.find(1)
- assert !firm.persisted?
+ assert_not_predicate firm, :persisted?
assert_equal a, firm.account
assert firm.save
assert_equal a, firm.account
@@ -167,12 +167,12 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
def test_assignment_before_either_saved
firm = Firm.new("name" => "GlobalMegaCorp")
firm.account = a = Account.new("credit_limit" => 1000)
- assert !firm.persisted?
- assert !a.persisted?
+ assert_not_predicate firm, :persisted?
+ assert_not_predicate a, :persisted?
assert_equal a, firm.account
assert firm.save
- assert firm.persisted?
- assert a.persisted?
+ assert_predicate firm, :persisted?
+ assert_predicate a, :persisted?
assert_equal a, firm.account
firm.association(:account).reload
assert_equal a, firm.account
@@ -221,13 +221,13 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
def test_should_save_parent_but_not_invalid_child
client = Client.new(name: "Joe (the Plumber)")
- assert client.valid?
+ assert_predicate client, :valid?
client.build_firm
- assert !client.firm.valid?
+ assert_not_predicate client.firm, :valid?
assert client.save
- assert !client.firm.persisted?
+ assert_not_predicate client.firm, :persisted?
end
def test_save_fails_for_invalid_belongs_to
@@ -235,9 +235,9 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
assert log = AuditLog.create(developer_id: 0, message: " ")
log.developer = Developer.new
- assert !log.developer.valid?
- assert !log.valid?
- assert !log.save
+ assert_not_predicate log.developer, :valid?
+ assert_not_predicate log, :valid?
+ assert_not log.save
assert_equal ["is invalid"], log.errors["developer"]
end
@@ -246,8 +246,8 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
assert log = AuditLog.create(developer_id: 0, message: " ")
log.unvalidated_developer = Developer.new
- assert !log.unvalidated_developer.valid?
- assert log.valid?
+ assert_not_predicate log.unvalidated_developer, :valid?
+ assert_predicate log, :valid?
assert log.save
end
@@ -256,10 +256,10 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
apple = Firm.new("name" => "Apple")
client.firm = apple
assert_equal apple, client.firm
- assert !apple.persisted?
+ assert_not_predicate apple, :persisted?
assert client.save
assert apple.save
- assert apple.persisted?
+ assert_predicate apple, :persisted?
assert_equal apple, client.firm
client.association(:firm).reload
assert_equal apple, client.firm
@@ -269,11 +269,11 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
final_cut = Client.new("name" => "Final Cut")
apple = Firm.new("name" => "Apple")
final_cut.firm = apple
- assert !final_cut.persisted?
- assert !apple.persisted?
+ assert_not_predicate final_cut, :persisted?
+ assert_not_predicate apple, :persisted?
assert final_cut.save
- assert final_cut.persisted?
- assert apple.persisted?
+ assert_predicate final_cut, :persisted?
+ assert_predicate apple, :persisted?
assert_equal apple, final_cut.firm
final_cut.association(:firm).reload
assert_equal apple, final_cut.firm
@@ -382,7 +382,7 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test
auditlog.developer = invalid_developer
auditlog.developer_id = valid_developer.id
- assert auditlog.valid?
+ assert_predicate auditlog, :valid?
end
end
@@ -395,8 +395,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
molecule.electrons = [valid_electron, invalid_electron]
molecule.save
- assert_not invalid_electron.valid?
- assert valid_electron.valid?
+ assert_not_predicate invalid_electron, :valid?
+ assert_predicate valid_electron, :valid?
assert_not molecule.persisted?, "Molecule should not be persisted when its electrons are invalid"
end
@@ -408,9 +408,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid]
- assert_not tuning_peg_invalid.valid?
- assert tuning_peg_valid.valid?
- assert_not guitar.valid?
+ assert_not_predicate tuning_peg_invalid, :valid?
+ assert_predicate tuning_peg_valid, :valid?
+ assert_not_predicate guitar, :valid?
assert_equal ["is not a number"], guitar.errors["tuning_pegs[1].pitch"]
assert_not_equal ["is not a number"], guitar.errors["tuning_pegs.pitch"]
end
@@ -425,9 +425,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
molecule.electrons = [valid_electron, invalid_electron]
- assert_not invalid_electron.valid?
- assert valid_electron.valid?
- assert_not molecule.valid?
+ assert_not_predicate invalid_electron, :valid?
+ assert_predicate valid_electron, :valid?
+ assert_not_predicate molecule, :valid?
assert_equal ["can't be blank"], molecule.errors["electrons[1].name"]
assert_not_equal ["can't be blank"], molecule.errors["electrons.name"]
ensure
@@ -441,9 +441,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
molecule.electrons = [valid_electron, invalid_electron]
- assert_not invalid_electron.valid?
- assert valid_electron.valid?
- assert_not molecule.valid?
+ assert_not_predicate invalid_electron, :valid?
+ assert_predicate valid_electron, :valid?
+ assert_not_predicate molecule, :valid?
assert_equal [{ error: :blank }], molecule.errors.details[:"electrons.name"]
end
@@ -455,9 +455,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid]
- assert_not tuning_peg_invalid.valid?
- assert tuning_peg_valid.valid?
- assert_not guitar.valid?
+ assert_not_predicate tuning_peg_invalid, :valid?
+ assert_predicate tuning_peg_valid, :valid?
+ assert_not_predicate guitar, :valid?
assert_equal [{ error: :not_a_number, value: nil }], guitar.errors.details[:"tuning_pegs[1].pitch"]
assert_equal [], guitar.errors.details[:"tuning_pegs.pitch"]
end
@@ -472,9 +472,9 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
molecule.electrons = [valid_electron, invalid_electron]
- assert_not invalid_electron.valid?
- assert valid_electron.valid?
- assert_not molecule.valid?
+ assert_not_predicate invalid_electron, :valid?
+ assert_predicate valid_electron, :valid?
+ assert_not_predicate molecule, :valid?
assert_equal [{ error: :blank }], molecule.errors.details[:"electrons[1].name"]
assert_equal [], molecule.errors.details[:"electrons.name"]
ensure
@@ -488,8 +488,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib
molecule.electrons = [valid_electron]
molecule.save
- assert valid_electron.valid?
- assert molecule.persisted?
+ assert_predicate valid_electron, :valid?
+ assert_predicate molecule, :persisted?
assert_equal 1, molecule.electrons.count
end
end
@@ -499,22 +499,22 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
def test_invalid_adding
firm = Firm.find(1)
- assert !(firm.clients_of_firm << c = Client.new)
- assert !c.persisted?
- assert !firm.valid?
- assert !firm.save
- assert !c.persisted?
+ assert_not (firm.clients_of_firm << c = Client.new)
+ assert_not_predicate c, :persisted?
+ assert_not_predicate firm, :valid?
+ assert_not firm.save
+ assert_not_predicate c, :persisted?
end
def test_invalid_adding_before_save
new_firm = Firm.new("name" => "A New Firm, Inc")
new_firm.clients_of_firm.concat([c = Client.new, Client.new("name" => "Apple")])
- assert !c.persisted?
- assert !c.valid?
- assert !new_firm.valid?
- assert !new_firm.save
- assert !c.persisted?
- assert !new_firm.persisted?
+ assert_not_predicate c, :persisted?
+ assert_not_predicate c, :valid?
+ assert_not_predicate new_firm, :valid?
+ assert_not new_firm.save
+ assert_not_predicate c, :persisted?
+ assert_not_predicate new_firm, :persisted?
end
def test_invalid_adding_with_validate_false
@@ -522,10 +522,10 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
client = Client.new
firm.unvalidated_clients_of_firm << client
- assert firm.valid?
- assert !client.valid?
+ assert_predicate firm, :valid?
+ assert_not_predicate client, :valid?
assert firm.save
- assert !client.persisted?
+ assert_not_predicate client, :persisted?
end
def test_valid_adding_with_validate_false
@@ -534,24 +534,24 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
firm = Firm.first
client = Client.new("name" => "Apple")
- assert firm.valid?
- assert client.valid?
- assert !client.persisted?
+ assert_predicate firm, :valid?
+ assert_predicate client, :valid?
+ assert_not_predicate client, :persisted?
firm.unvalidated_clients_of_firm << client
assert firm.save
- assert client.persisted?
+ assert_predicate client, :persisted?
assert_equal no_of_clients + 1, Client.count
end
def test_invalid_build
new_client = companies(:first_firm).clients_of_firm.build
- assert !new_client.persisted?
- assert !new_client.valid?
+ 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 !new_client.persisted?
+ assert_not companies(:first_firm).save
+ assert_not_predicate new_client, :persisted?
assert_equal 2, companies(:first_firm).clients_of_firm.reload.size
end
@@ -570,8 +570,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
assert_equal no_of_firms, Firm.count # Firm was not saved to database.
assert_equal no_of_clients, Client.count # Clients were not saved to database.
assert new_firm.save
- assert new_firm.persisted?
- assert c.persisted?
+ assert_predicate new_firm, :persisted?
+ assert_predicate c, :persisted?
assert_equal new_firm, c.firm
assert_equal no_of_firms + 1, Firm.count # Firm was saved to database.
assert_equal no_of_clients + 2, Client.count # Clients were saved to database.
@@ -601,11 +601,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") }
- assert !company.clients_of_firm.loaded?
+ assert_not_predicate company.clients_of_firm, :loaded?
company.name += "-changed"
assert_queries(2) { assert company.save }
- assert new_client.persisted?
+ assert_predicate new_client, :persisted?
assert_equal 3, company.clients_of_firm.reload.size
end
@@ -621,11 +621,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" } }
- assert !company.clients_of_firm.loaded?
+ assert_not_predicate company.clients_of_firm, :loaded?
company.name += "-changed"
assert_queries(2) { assert company.save }
- assert new_client.persisted?
+ assert_predicate new_client, :persisted?
assert_equal 3, company.clients_of_firm.reload.size
end
@@ -657,62 +657,62 @@ class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase
new_account = Account.new("credit_limit" => 1000)
new_firm = Firm.new("name" => "some firm")
- assert !new_firm.persisted?
+ assert_not_predicate new_firm, :persisted?
new_account.firm = new_firm
new_account.save!
- assert new_firm.persisted?
+ assert_predicate new_firm, :persisted?
new_account = Account.new("credit_limit" => 1000)
new_autosaved_firm = Firm.new("name" => "some firm")
- assert !new_autosaved_firm.persisted?
+ assert_not_predicate new_autosaved_firm, :persisted?
new_account.unautosaved_firm = new_autosaved_firm
new_account.save!
- assert !new_autosaved_firm.persisted?
+ assert_not_predicate new_autosaved_firm, :persisted?
end
def test_autosave_new_record_on_has_one_can_be_disabled_per_relationship
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
firm.account = account
firm.save!
- assert account.persisted?
+ assert_predicate account, :persisted?
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
firm.unautosaved_account = account
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
firm.unautosaved_account = account
firm.save!
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
end
def test_autosave_new_record_on_has_many_can_be_disabled_per_relationship
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
firm.accounts << account
firm.save!
- assert account.persisted?
+ assert_predicate account, :persisted?
firm = Firm.new("name" => "some firm")
account = Account.new("credit_limit" => 1000)
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
firm.unautosaved_accounts << account
firm.save!
- assert !account.persisted?
+ assert_not_predicate account, :persisted?
end
def test_autosave_new_record_with_after_create_callback
@@ -745,18 +745,18 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.mark_for_destruction
@pirate.ship.mark_for_destruction
- assert !@pirate.reload.marked_for_destruction?
- assert !@pirate.ship.reload.marked_for_destruction?
+ assert_not_predicate @pirate.reload, :marked_for_destruction?
+ assert_not_predicate @pirate.ship.reload, :marked_for_destruction?
end
# has_one
def test_should_destroy_a_child_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction
- assert !@pirate.ship.marked_for_destruction?
+ assert_not_predicate @pirate.ship, :marked_for_destruction?
@pirate.ship.mark_for_destruction
id = @pirate.ship.id
- assert @pirate.ship.marked_for_destruction?
+ assert_predicate @pirate.ship, :marked_for_destruction?
assert Ship.find_by_id(id)
@pirate.save
@@ -766,11 +766,12 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
def test_should_skip_validation_on_a_child_association_if_marked_for_destruction
@pirate.ship.name = ""
- assert !@pirate.valid?
+ 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 +796,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,18 +807,19 @@ 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
def test_should_destroy_a_parent_association_as_part_of_the_save_transaction_if_it_was_marked_for_destruction
- assert !@ship.pirate.marked_for_destruction?
+ assert_not_predicate @ship.pirate, :marked_for_destruction?
@ship.pirate.mark_for_destruction
id = @ship.pirate.id
- assert @ship.pirate.marked_for_destruction?
+ assert_predicate @ship.pirate, :marked_for_destruction?
assert Pirate.find_by_id(id)
@ship.save
@@ -827,11 +829,12 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
def test_should_skip_validation_on_a_parent_association_if_marked_for_destruction
@ship.pirate.catchphrase = ""
- assert !@ship.valid?
+ 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 +858,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 +874,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
@@ -881,7 +884,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
ids.each { |id| assert klass.find_by_id(id) }
@pirate.save
- assert @pirate.reload.birds.empty?
+ assert_empty @pirate.reload.birds
ids.each { |id| assert_nil klass.find_by_id(id) }
end
@@ -889,30 +892,32 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.birds.create!(name: :parrot)
@pirate.birds.first.destroy
@pirate.save!
- assert @pirate.reload.birds.empty?
+ assert_empty @pirate.reload.birds
end
def test_should_skip_validation_on_has_many_if_marked_for_destruction
2.times { |i| @pirate.birds.create!(name: "birds_#{i}") }
@pirate.birds.each { |bird| bird.name = "" }
- assert !@pirate.valid?
+ 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
@pirate.birds.create!(name: "birds_1")
@pirate.birds.each { |bird| bird.name = "" }
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
@pirate.birds.each(&:destroy)
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_has_many
@@ -921,8 +926,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 +945,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,42 +1011,44 @@ 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
@pirate.save
end
- assert @pirate.reload.parrots.empty?
+ assert_empty @pirate.reload.parrots
join_records = Pirate.connection.select_all("SELECT * FROM parrots_pirates WHERE pirate_id = #{@pirate.id}")
- assert join_records.empty?
+ assert_empty join_records
end
def test_should_skip_validation_on_habtm_if_marked_for_destruction
2.times { |i| @pirate.parrots.create!(name: "parrots_#{i}") }
@pirate.parrots.each { |parrot| parrot.name = "" }
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
+
+ @pirate.parrots.each { |parrot| parrot.mark_for_destruction }
- @pirate.parrots.each do |parrot|
- parrot.mark_for_destruction
- parrot.expects(:valid?).never
+ assert_not_called(@pirate.parrots.first, :valid?) do
+ assert_not_called(@pirate.parrots.last, :valid?) do
+ @pirate.save!
+ end
end
- @pirate.save!
- assert @pirate.reload.parrots.empty?
+ assert_empty @pirate.reload.parrots
end
def test_should_skip_validation_on_habtm_if_destroyed
@pirate.parrots.create!(name: "parrots_1")
@pirate.parrots.each { |parrot| parrot.name = "" }
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
@pirate.parrots.each(&:destroy)
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
end
def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_habtm
@@ -1065,7 +1075,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
@@ -1145,16 +1155,16 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_automatically_validate_the_associated_model
@pirate.ship.name = ""
- assert @pirate.invalid?
- assert @pirate.errors[:"ship.name"].any?
+ assert_predicate @pirate, :invalid?
+ assert_predicate @pirate.errors[:"ship.name"], :any?
end
def test_should_merge_errors_on_the_associated_models_onto_the_parent_even_if_it_is_not_valid
@pirate.ship.name = nil
@pirate.catchphrase = nil
- assert @pirate.invalid?
- assert @pirate.errors[:"ship.name"].any?
- assert @pirate.errors[:catchphrase].any?
+ assert_predicate @pirate, :invalid?
+ assert_predicate @pirate.errors[:"ship.name"], :any?
+ assert_predicate @pirate.errors[:catchphrase], :any?
end
def test_should_not_ignore_different_error_messages_on_the_same_attribute
@@ -1163,7 +1173,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
Ship.validates_format_of :name, with: /\w/
@pirate.ship.name = ""
@pirate.catchphrase = nil
- assert @pirate.invalid?
+ assert_predicate @pirate, :invalid?
assert_equal ["can't be blank", "is invalid"], @pirate.errors[:"ship.name"]
ensure
Ship._validators = old_validators if old_validators
@@ -1213,7 +1223,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 +1242,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
@@ -1244,7 +1254,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
ship = ShipWithoutNestedAttributes.new(name: "The Black Flag")
ship.parts.build.mark_for_destruction
- assert_not ship.valid?
+ assert_not_predicate ship, :valid?
end
end
@@ -1299,16 +1309,16 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_automatically_validate_the_associated_model
@ship.pirate.catchphrase = ""
- assert @ship.invalid?
- assert @ship.errors[:"pirate.catchphrase"].any?
+ assert_predicate @ship, :invalid?
+ assert_predicate @ship.errors[:"pirate.catchphrase"], :any?
end
def test_should_merge_errors_on_the_associated_model_onto_the_parent_even_if_it_is_not_valid
@ship.name = nil
@ship.pirate.catchphrase = nil
- assert @ship.invalid?
- assert @ship.errors[:name].any?
- assert @ship.errors[:"pirate.catchphrase"].any?
+ assert_predicate @ship, :invalid?
+ assert_predicate @ship.errors[:name], :any?
+ assert_predicate @ship.errors[:"pirate.catchphrase"], :any?
end
def test_should_still_allow_to_bypass_validations_on_the_associated_model
@@ -1337,7 +1347,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 +1366,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
@@ -1403,17 +1413,17 @@ module AutosaveAssociationOnACollectionAssociationTests
def test_should_automatically_validate_the_associated_models
@pirate.send(@association_name).each { |child| child.name = "" }
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
- assert @pirate.errors[@association_name].empty?
+ assert_empty @pirate.errors[@association_name]
end
def test_should_not_use_default_invalid_error_on_associated_models
@pirate.send(@association_name).build(name: "")
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
- assert @pirate.errors[@association_name].empty?
+ assert_empty @pirate.errors[@association_name]
end
def test_should_default_invalid_error_from_i18n
@@ -1423,10 +1433,10 @@ module AutosaveAssociationOnACollectionAssociationTests
@pirate.send(@association_name).build(name: "")
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
assert_equal ["cannot be blank"], @pirate.errors["#{@association_name}.name"]
assert_equal ["#{@association_name.to_s.humanize} name cannot be blank"], @pirate.errors.full_messages
- assert @pirate.errors[@association_name].empty?
+ assert_empty @pirate.errors[@association_name]
ensure
I18n.backend = I18n::Backend::Simple.new
end
@@ -1435,9 +1445,9 @@ module AutosaveAssociationOnACollectionAssociationTests
@pirate.send(@association_name).each { |child| child.name = "" }
@pirate.catchphrase = nil
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
assert_equal ["can't be blank"], @pirate.errors["#{@association_name}.name"]
- assert @pirate.errors[:catchphrase].any?
+ assert_predicate @pirate.errors[:catchphrase], :any?
end
def test_should_allow_to_bypass_validations_on_the_associated_models_on_update
@@ -1480,7 +1490,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 +1500,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 +1520,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
@@ -1595,10 +1605,10 @@ class TestAutosaveAssociationValidationsOnAHasManyAssociation < ActiveRecord::Te
end
test "should automatically validate associations" do
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
@pirate.birds.each { |bird| bird.name = "" }
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
end
end
@@ -1613,15 +1623,15 @@ class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::Tes
end
test "should automatically validate associations with :validate => true" do
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
@pirate.ship.name = ""
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
end
test "should not automatically add validate associations without :validate => true" do
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
@pirate.non_validated_ship.name = ""
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
end
end
@@ -1634,15 +1644,15 @@ class TestAutosaveAssociationValidationsOnABelongsToAssociation < ActiveRecord::
end
test "should automatically validate associations with :validate => true" do
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
@pirate.parrot = Parrot.new(name: "")
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
end
test "should not automatically validate associations without :validate => true" do
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
@pirate.non_validated_parrot = Parrot.new(name: "")
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
end
end
@@ -1655,17 +1665,17 @@ class TestAutosaveAssociationValidationsOnAHABTMAssociation < ActiveRecord::Test
end
test "should automatically validate associations with :validate => true" do
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
@pirate.parrots = [ Parrot.new(name: "popuga") ]
@pirate.parrots.each { |parrot| parrot.name = "" }
- assert !@pirate.valid?
+ assert_not_predicate @pirate, :valid?
end
test "should not automatically validate associations without :validate => true" do
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
@pirate.non_validated_parrots = [ Parrot.new(name: "popuga") ]
@pirate.non_validated_parrots.each { |parrot| parrot.name = "" }
- assert @pirate.valid?
+ assert_predicate @pirate, :valid?
end
end
@@ -1686,7 +1696,7 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas
end
test "should not generate validation methods for has_one associations without :validate => true" do
- assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_ship)
+ assert_not_respond_to @pirate, :validate_associated_records_for_non_validated_ship
end
test "should generate validation methods for belongs_to associations with :validate => true" do
@@ -1694,7 +1704,7 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas
end
test "should not generate validation methods for belongs_to associations without :validate => true" do
- assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_parrot)
+ assert_not_respond_to @pirate, :validate_associated_records_for_non_validated_parrot
end
test "should generate validation methods for HABTM associations with :validate => true" do
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 1a1d4ce039..fd008ca8e3 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -104,7 +104,7 @@ class BasicsTest < ActiveRecord::TestCase
pk = Author.columns_hash["id"]
ref = Post.columns_hash["author_id"]
- assert_equal pk.bigint?, ref.bigint?
+ assert_equal pk.sql_type, ref.sql_type
end
def test_many_mutations
@@ -147,8 +147,8 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_table_exists
- assert !NonExistentTable.table_exists?
- assert Topic.table_exists?
+ assert_not_predicate NonExistentTable, :table_exists?
+ assert_predicate Topic, :table_exists?
end
def test_preserving_date_objects
@@ -307,7 +307,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
@@ -450,7 +450,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_default_values
topic = Topic.new
- assert topic.approved?
+ assert_predicate topic, :approved?
assert_nil topic.written_on
assert_nil topic.bonus_time
assert_nil topic.last_read
@@ -458,7 +458,7 @@ class BasicsTest < ActiveRecord::TestCase
topic.save
topic = Topic.find(topic.id)
- assert topic.approved?
+ assert_predicate topic, :approved?
assert_nil topic.last_read
# Oracle has some funky default handling, so it requires a bit of
@@ -629,7 +629,7 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_readonly_attributes
- assert_equal Set.new([ "title" , "comments_count" ]), ReadonlyTitlePost.readonly_attributes
+ assert_equal Set.new([ "title", "comments_count" ]), ReadonlyTitlePost.readonly_attributes
post = ReadonlyTitlePost.create(title: "cannot change this", body: "changeable")
post.reload
@@ -727,9 +727,9 @@ class BasicsTest < ActiveRecord::TestCase
b_nil = Boolean.find(nil_id)
assert_nil b_nil.value
b_false = Boolean.find(false_id)
- assert !b_false.value?
+ assert_not_predicate b_false, :value?
b_true = Boolean.find(true_id)
- assert b_true.value?
+ assert_predicate b_true, :value?
end
def test_boolean_without_questionmark
@@ -753,9 +753,9 @@ class BasicsTest < ActiveRecord::TestCase
b_blank = Boolean.find(blank_id)
assert_nil b_blank.value
b_false = Boolean.find(false_id)
- assert !b_false.value?
+ assert_not_predicate b_false, :value?
b_true = Boolean.find(true_id)
- assert b_true.value?
+ assert_predicate b_true, :value?
end
def test_new_record_returns_boolean
@@ -768,7 +768,7 @@ class BasicsTest < ActiveRecord::TestCase
duped_topic = nil
assert_nothing_raised { duped_topic = topic.dup }
assert_equal topic.title, duped_topic.title
- assert !duped_topic.persisted?
+ assert_not_predicate duped_topic, :persisted?
# test if the attributes have been duped
topic.title = "a"
@@ -786,7 +786,7 @@ class BasicsTest < ActiveRecord::TestCase
# test if saved clone object differs from original
duped_topic.save
- assert duped_topic.persisted?
+ assert_predicate duped_topic, :persisted?
assert_not_equal duped_topic.id, topic.id
duped_topic.reload
@@ -807,7 +807,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_nothing_raised { dup = dev.dup }
assert_kind_of DeveloperSalary, dup.salary
assert_equal dev.salary.amount, dup.salary.amount
- assert !dup.persisted?
+ assert_not_predicate dup, :persisted?
# test if the attributes have been duped
original_amount = dup.salary.amount
@@ -815,7 +815,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal original_amount, dup.salary.amount
assert dup.save
- assert dup.persisted?
+ assert_predicate dup, :persisted?
assert_not_equal dup.id, dev.id
end
@@ -835,52 +835,52 @@ class BasicsTest < ActiveRecord::TestCase
def test_clone_of_new_object_with_defaults
developer = Developer.new
- assert !developer.name_changed?
- assert !developer.salary_changed?
+ assert_not_predicate developer, :name_changed?
+ assert_not_predicate developer, :salary_changed?
cloned_developer = developer.clone
- assert !cloned_developer.name_changed?
- assert !cloned_developer.salary_changed?
+ assert_not_predicate cloned_developer, :name_changed?
+ assert_not_predicate cloned_developer, :salary_changed?
end
def test_clone_of_new_object_marks_attributes_as_dirty
developer = Developer.new name: "Bjorn", salary: 100000
- assert developer.name_changed?
- assert developer.salary_changed?
+ assert_predicate developer, :name_changed?
+ assert_predicate developer, :salary_changed?
cloned_developer = developer.clone
- assert cloned_developer.name_changed?
- assert cloned_developer.salary_changed?
+ assert_predicate cloned_developer, :name_changed?
+ assert_predicate cloned_developer, :salary_changed?
end
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 cloned_developer.name_changed?
- assert !cloned_developer.salary_changed? # ... and cloned instance should behave same
+ assert_predicate cloned_developer, :name_changed?
+ assert_not cloned_developer.salary_changed? # ... and cloned instance should behave same
end
def test_dup_of_saved_object_marks_attributes_as_dirty
developer = Developer.create! name: "Bjorn", salary: 100000
- assert !developer.name_changed?
- assert !developer.salary_changed?
+ assert_not_predicate developer, :name_changed?
+ assert_not_predicate developer, :salary_changed?
cloned_developer = developer.dup
assert cloned_developer.name_changed? # both attributes differ from defaults
- assert cloned_developer.salary_changed?
+ assert_predicate cloned_developer, :salary_changed?
end
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 !developer.salary_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
@@ -891,11 +891,9 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal 2147483648, company.rating
end
- unless current_adapter?(:SQLite3Adapter)
- def test_bignum_pk
- company = Company.create!(id: 2147483648, name: "foo")
- assert_equal company, Company.find(company.id)
- end
+ def test_bignum_pk
+ company = Company.create!(id: 2147483648, name: "foo")
+ assert_equal company, Company.find(company.id)
end
# TODO: extend defaults tests to other databases!
@@ -953,14 +951,14 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_toggle_attribute
- assert !topics(:first).approved?
+ assert_not_predicate topics(:first), :approved?
topics(:first).toggle!(:approved)
- assert topics(:first).approved?
+ assert_predicate topics(:first), :approved?
topic = topics(:first)
topic.toggle(:approved)
- assert !topic.approved?
+ assert_not_predicate topic, :approved?
topic.reload
- assert topic.approved?
+ assert_predicate topic, :approved?
end
def test_reload
@@ -1433,28 +1431,92 @@ class BasicsTest < ActiveRecord::TestCase
test "resetting column information doesn't remove attribute methods" do
topic = topics(:first)
- assert_not topic.id_changed?
+ assert_not_predicate topic, :id_changed?
Topic.reset_column_information
- assert_not topic.id_changed?
+ assert_not_predicate topic, :id_changed?
end
test "ignored columns are not present in columns_hash" do
cache_columns = Developer.connection.schema_cache.columns_hash(Developer.table_name)
assert_includes cache_columns.keys, "first_name"
assert_not_includes Developer.columns_hash.keys, "first_name"
+ assert_not_includes SubDeveloper.columns_hash.keys, "first_name"
+ assert_not_includes SymbolIgnoredDeveloper.columns_hash.keys, "first_name"
end
test "ignored columns have no attribute methods" do
- refute Developer.new.respond_to?(:first_name)
- refute Developer.new.respond_to?(:first_name=)
- refute Developer.new.respond_to?(:first_name?)
+ assert_not_respond_to Developer.new, :first_name
+ assert_not_respond_to Developer.new, :first_name=
+ assert_not_respond_to Developer.new, :first_name?
+ assert_not_respond_to SubDeveloper.new, :first_name
+ assert_not_respond_to SubDeveloper.new, :first_name=
+ assert_not_respond_to SubDeveloper.new, :first_name?
+ assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name
+ assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name=
+ assert_not_respond_to SymbolIgnoredDeveloper.new, :first_name?
end
test "ignored columns don't prevent explicit declaration of attribute methods" do
- assert Developer.new.respond_to?(:last_name)
- assert Developer.new.respond_to?(:last_name=)
- assert Developer.new.respond_to?(:last_name?)
+ assert_respond_to Developer.new, :last_name
+ assert_respond_to Developer.new, :last_name=
+ assert_respond_to Developer.new, :last_name?
+ assert_respond_to SubDeveloper.new, :last_name
+ assert_respond_to SubDeveloper.new, :last_name=
+ assert_respond_to SubDeveloper.new, :last_name?
+ assert_respond_to SymbolIgnoredDeveloper.new, :last_name
+ assert_respond_to SymbolIgnoredDeveloper.new, :last_name=
+ assert_respond_to SymbolIgnoredDeveloper.new, :last_name?
+ end
+
+ test "ignored columns are stored as an array of string" do
+ assert_equal(%w(first_name last_name), Developer.ignored_columns)
+ assert_equal(%w(first_name last_name), SymbolIgnoredDeveloper.ignored_columns)
+ end
+
+ test "when #reload called, ignored columns' attribute methods are not defined" do
+ developer = Developer.create!(name: "Developer")
+ assert_not_respond_to developer, :first_name
+ assert_not_respond_to developer, :first_name=
+
+ developer.reload
+
+ assert_not_respond_to developer, :first_name
+ assert_not_respond_to developer, :first_name=
+ end
+
+ test "ignored columns not included in SELECT" do
+ query = Developer.all.to_sql.downcase
+
+ # ignored column
+ assert_not query.include?("first_name")
+
+ # regular column
+ assert query.include?("name")
+ end
+
+ test "column names are quoted when using #from clause and model has ignored columns" do
+ assert_not_empty Developer.ignored_columns
+ query = Developer.from("developers").to_sql
+ quoted_id = "#{Developer.quoted_table_name}.#{Developer.quoted_primary_key}"
+
+ assert_match(/SELECT #{Regexp.escape(quoted_id)}.* FROM developers/, query)
+ end
+
+ test "using table name qualified column names unless having SELECT list explicitly" do
+ assert_equal developers(:david), Developer.from("developers").joins(:shared_computers).take
+ end
+
+ test "protected environments by default is an array with production" do
+ assert_equal ["production"], ActiveRecord::Base.protected_environments
+ end
+
+ def test_protected_environments_are_stored_as_an_array_of_string
+ previous_protected_environments = ActiveRecord::Base.protected_environments
+ ActiveRecord::Base.protected_environments = [:staging, "production"]
+ assert_equal ["staging", "production"], ActiveRecord::Base.protected_environments
+ ensure
+ ActiveRecord::Base.protected_environments = previous_protected_environments
end
end
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index c965404b07..c8163901c6 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -313,7 +313,7 @@ class EachTest < ActiveRecord::TestCase
def test_in_batches_each_record_should_yield_record_if_block_is_given
assert_queries(6) do
Post.in_batches(of: 2).each_record do |post|
- assert post.title.present?
+ assert_predicate post.title, :present?
assert_kind_of Post, post
end
end
@@ -322,7 +322,7 @@ class EachTest < ActiveRecord::TestCase
def test_in_batches_each_record_should_return_enumerator_if_no_block_given
assert_queries(6) do
Post.in_batches(of: 2).each_record.with_index do |post, i|
- assert post.title.present?
+ assert_predicate post.title, :present?
assert_kind_of Post, post
end
end
@@ -353,24 +353,24 @@ class EachTest < ActiveRecord::TestCase
def test_in_batches_should_not_be_loaded
Post.in_batches(of: 1) do |relation|
- assert_not relation.loaded?
+ assert_not_predicate relation, :loaded?
end
Post.in_batches(of: 1, load: false) do |relation|
- assert_not relation.loaded?
+ assert_not_predicate relation, :loaded?
end
end
def test_in_batches_should_be_loaded
Post.in_batches(of: 1, load: true) do |relation|
- assert relation.loaded?
+ assert_predicate relation, :loaded?
end
end
def test_in_batches_if_not_loaded_executes_more_queries
assert_queries(@total + 1) do
Post.in_batches(of: 1, load: false) do |relation|
- assert_not relation.loaded?
+ assert_not_predicate relation, :loaded?
end
end
end
@@ -508,7 +508,7 @@ class EachTest < ActiveRecord::TestCase
def test_in_batches_relations_update_all_should_not_affect_matching_records_in_other_batches
Post.update_all(author_id: 0)
person = Post.last
- person.update_attributes(author_id: 1)
+ person.update(author_id: 1)
Post.in_batches(of: 2) do |batch|
batch.where("author_id >= 1").update_all("author_id = author_id + 1")
@@ -586,41 +586,17 @@ class EachTest < ActiveRecord::TestCase
end
end
- test ".error_on_ignored_order_or_limit= is deprecated" do
- begin
- prev = ActiveRecord::Base.error_on_ignored_order
- assert_deprecated "Please use error_on_ignored_order= instead." do
- ActiveRecord::Base.error_on_ignored_order_or_limit = true
- end
- assert ActiveRecord::Base.error_on_ignored_order
- ensure
- ActiveRecord::Base.error_on_ignored_order = prev
- end
- end
-
- test ".error_on_ignored_order_or_limit is deprecated" do
- expected = ActiveRecord::Base.error_on_ignored_order
- actual = assert_deprecated "Please use error_on_ignored_order instead." do
- ActiveRecord::Base.error_on_ignored_order_or_limit
- end
- assert_equal expected, actual
- end
-
- test "#error_on_ignored_order_or_limit is deprecated" do
- expected = ActiveRecord::Base.error_on_ignored_order
- actual = assert_deprecated "Please use error_on_ignored_order instead." do
- Post.new.error_on_ignored_order_or_limit
- end
- assert_equal expected, actual
- end
-
test ".find_each respects table alias" do
assert_queries(1) do
table_alias = Post.arel_table.alias("omg_posts")
table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias)
predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
- posts = ActiveRecord::Relation.create(Post, table_alias, predicate_builder)
+ posts = ActiveRecord::Relation.create(
+ Post,
+ table: table_alias,
+ predicate_builder: predicate_builder
+ )
posts.find_each {}
end
end
diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb
index 8f2f2c6186..3a569f226e 100644
--- a/activerecord/test/cases/cache_key_test.rb
+++ b/activerecord/test/cases/cache_key_test.rb
@@ -38,8 +38,8 @@ module ActiveRecord
end
test "cache_version is only there when versioning is on" do
- assert CacheMeWithVersion.create.cache_version.present?
- assert_not CacheMe.create.cache_version.present?
+ assert_predicate CacheMeWithVersion.create.cache_version, :present?
+ assert_not_predicate CacheMe.create.cache_version, :present?
end
test "cache_key_with_version always has both key and version" do
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index b47fd0af41..080d2a54bc 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -21,7 +21,7 @@ require "models/comment"
require "models/rating"
class CalculationsTest < ActiveRecord::TestCase
- fixtures :companies, :accounts, :topics, :speedometers, :minivans, :books
+ fixtures :companies, :accounts, :topics, :speedometers, :minivans, :books, :posts, :comments
def test_should_sum_field
assert_equal 318, Account.sum(:credit_limit)
@@ -236,6 +236,24 @@ class CalculationsTest < ActiveRecord::TestCase
end
end
+ def test_count_with_eager_loading_and_custom_order
+ posts = Post.includes(:comments).order("comments.id")
+ 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 }
+ assert_queries(1) { assert_equal 11, posts.count(:all) }
+ end
+
+ def test_distinct_count_all_with_custom_select_and_order
+ accounts = Account.distinct.select("credit_limit % 10").order(Arel.sql("credit_limit % 10"))
+ assert_queries(1) { assert_equal 3, accounts.count(:all) }
+ assert_queries(1) { assert_equal 3, accounts.load.size }
+ end
+
def test_distinct_count_with_order_and_limit
assert_equal 4, Account.distinct.order(:firm_id).limit(4).count
end
@@ -260,6 +278,10 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 3, Account.joins(:firm).distinct.order(:firm_id).limit(3).offset(2).count
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
+
def test_should_group_by_summed_field_having_condition
c = Account.group(:firm_id).having("sum(credit_limit) > 50").sum(:credit_limit)
assert_nil c[1]
@@ -659,14 +681,14 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_pluck_with_selection_clause
- assert_equal [50, 53, 55, 60], Account.pluck("DISTINCT credit_limit").sort
- assert_equal [50, 53, 55, 60], Account.pluck("DISTINCT accounts.credit_limit").sort
- assert_equal [50, 53, 55, 60], Account.pluck("DISTINCT(credit_limit)").sort
+ assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT credit_limit")).sort
+ assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT accounts.credit_limit")).sort
+ assert_equal [50, 53, 55, 60], Account.pluck(Arel.sql("DISTINCT(credit_limit)")).sort
# MySQL returns "SUM(DISTINCT(credit_limit))" as the column name unless
# an alias is provided. Without the alias, the column cannot be found
# and properly typecast.
- assert_equal [50 + 53 + 55 + 60], Account.pluck("SUM(DISTINCT(credit_limit)) as credit_limit")
+ assert_equal [50 + 53 + 55 + 60], Account.pluck(Arel.sql("SUM(DISTINCT(credit_limit)) as credit_limit"))
end
def test_plucks_with_ids
@@ -678,6 +700,11 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal [], Topic.includes(:replies).limit(1).where("0 = 1").pluck(:id)
end
+ def test_pluck_with_includes_offset
+ assert_equal [5], Topic.includes(:replies).order(:id).offset(4).pluck(:id)
+ assert_equal [], Topic.includes(:replies).order(:id).offset(5).pluck(:id)
+ 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)
@@ -768,10 +795,27 @@ class CalculationsTest < ActiveRecord::TestCase
companies = Company.order(:name).limit(3).load
assert_queries 1 do
- assert_equal ["37signals", "Apex", "Ex Nihilo"], companies.pluck("DISTINCT name")
+ assert_equal ["37signals", "Apex", "Ex Nihilo"], companies.pluck(Arel.sql("DISTINCT name"))
end
end
+ 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)
+ 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)
+ end
+
+ def test_pick_delegate_to_all
+ cool_first = minivans(:cool_first)
+ assert_equal cool_first.color, Minivan.pick(:color)
+ end
+
def test_grouped_calculation_with_polymorphic_relation
part = ShipPart.create!(name: "has trinket")
part.trinkets.create!
diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb
index 55c7475f46..b9ba51c730 100644
--- a/activerecord/test/cases/callbacks_test.rb
+++ b/activerecord/test/cases/callbacks_test.rb
@@ -385,60 +385,60 @@ 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
def test_before_create_throwing_abort
someone = CallbackHaltedDeveloper.new
someone.cancel_before_create = true
- assert someone.valid?
- assert !someone.save
+ assert_predicate someone, :valid?
+ assert_not someone.save
assert_save_callbacks_not_called(someone)
end
def test_before_save_throwing_abort
david = DeveloperWithCanceledCallbacks.find(1)
- assert david.valid?
- assert !david.save
+ assert_predicate david, :valid?
+ 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 !david.valid?
- assert !david.save
+ assert_not_predicate david, :valid?
+ assert_not david.save
assert_raise(ActiveRecord::RecordInvalid) { david.save! }
someone = CallbackHaltedDeveloper.find(1)
someone.cancel_before_save = true
- assert someone.valid?
- assert !someone.save
+ assert_predicate someone, :valid?
+ assert_not someone.save
assert_save_callbacks_not_called(someone)
end
def test_before_update_throwing_abort
someone = CallbackHaltedDeveloper.find(1)
someone.cancel_before_update = true
- assert someone.valid?
- assert !someone.save
+ assert_predicate someone, :valid?
+ 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,12 +467,12 @@ 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
diff --git a/activerecord/test/cases/clone_test.rb b/activerecord/test/cases/clone_test.rb
index 3187e6aed5..65e5016040 100644
--- a/activerecord/test/cases/clone_test.rb
+++ b/activerecord/test/cases/clone_test.rb
@@ -36,7 +36,7 @@ module ActiveRecord
cloned = Topic.new
clone = cloned.clone
cloned.freeze
- assert_not clone.frozen?
+ assert_not_predicate clone, :frozen?
end
end
end
diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb
index c137693211..a5d908344a 100644
--- a/activerecord/test/cases/collection_cache_key_test.rb
+++ b/activerecord/test/cases/collection_cache_key_test.rb
@@ -24,7 +24,7 @@ module ActiveRecord
/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key
- assert_equal Digest::MD5.hexdigest(developers.to_sql), $1
+ assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1
assert_equal developers.count.to_s, $2
assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
end
@@ -37,7 +37,7 @@ module ActiveRecord
/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key
- assert_equal Digest::MD5.hexdigest(developers.to_sql), $1
+ assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1
assert_equal developers.count.to_s, $2
assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
end
@@ -50,7 +50,7 @@ module ActiveRecord
/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key
- assert_equal Digest::MD5.hexdigest(developers.to_sql), $1
+ assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1
assert_equal developers.count.to_s, $2
assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
end
@@ -60,7 +60,11 @@ module ActiveRecord
table_metadata = ActiveRecord::TableMetadata.new(Developer, table_alias)
predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
- developers = ActiveRecord::Relation.create(Developer, table_alias, predicate_builder)
+ developers = ActiveRecord::Relation.create(
+ Developer,
+ table: table_alias,
+ predicate_builder: predicate_builder
+ )
developers = developers.where(salary: 100000).order(updated_at: :desc)
last_developer_timestamp = developers.first.updated_at
@@ -68,11 +72,21 @@ module ActiveRecord
/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key
- assert_equal Digest::MD5.hexdigest(developers.to_sql), $1
+ assert_equal ActiveSupport::Digest.hexdigest(developers.to_sql), $1
assert_equal developers.count.to_s, $2
assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3
end
+ test "cache_key for relation with includes" do
+ comments = Comment.includes(:post).where("posts.type": "Post")
+ assert_match(/\Acomments\/query-(\h+)-(\d+)-(\d+)\z/, comments.cache_key)
+ end
+
+ test "cache_key for loaded relation with includes" do
+ comments = Comment.includes(:post).where("posts.type": "Post").load
+ assert_match(/\Acomments\/query-(\h+)-(\d+)-(\d+)\z/, comments.cache_key)
+ end
+
test "it triggers at most one query" do
developers = Developer.where(name: "David")
@@ -131,5 +145,17 @@ module ActiveRecord
assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
end
+
+ test "cache_key with a relation having distinct and order" do
+ developers = Developer.distinct.order(:salary).limit(5)
+
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
+ end
+
+ test "cache_key with a relation having custom select and order" do
+ developers = Developer.select("name AS dev_name").order("dev_name DESC").limit(5)
+
+ assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key)
+ end
end
end
diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb
index 1bcafd4b55..584e03d196 100644
--- a/activerecord/test/cases/comment_test.rb
+++ b/activerecord/test/cases/comment_test.rb
@@ -142,5 +142,27 @@ if ActiveRecord::Base.connection.supports_comments?
assert_match %r[t\.string\s+"absent_comment"\n], output
assert_no_match %r[t\.string\s+"absent_comment", comment:\n], output
end
+
+ def test_change_table_comment
+ @connection.change_table_comment :commenteds, "Edited table comment"
+ assert_equal "Edited table comment", @connection.table_comment("commenteds")
+ end
+
+ def test_change_table_comment_to_nil
+ @connection.change_table_comment :commenteds, nil
+ assert_nil @connection.table_comment("commenteds")
+ end
+
+ def test_change_column_comment
+ @connection.change_column_comment :commenteds, :name, "Edited column comment"
+ column = Commented.columns_hash["name"]
+ assert_equal "Edited column comment", column.comment
+ end
+
+ def test_change_column_comment_to_nil
+ @connection.change_column_comment :commenteds, :name, nil
+ column = Commented.columns_hash["name"]
+ assert_nil column.comment
+ end
end
end
diff --git a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb
index 82c6cf8dea..72838ff56b 100644
--- a/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb
+++ b/activerecord/test/cases/connection_adapters/adapter_leasing_test.rb
@@ -45,11 +45,11 @@ module ActiveRecord
# Make sure the pool marks the connection in use
assert_equal @adapter, pool.connection
- assert @adapter.in_use?
+ assert_predicate @adapter, :in_use?
# Close should put the adapter back in the pool
@adapter.close
- assert_not @adapter.in_use?
+ assert_not_predicate @adapter, :in_use?
assert_equal @adapter, pool.connection
end
diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
index 74d0ed348e..b8e623f17b 100644
--- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
@@ -1,10 +1,15 @@
# frozen_string_literal: true
require "cases/helper"
+require "models/person"
module ActiveRecord
module ConnectionAdapters
class ConnectionHandlerTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ fixtures :people
+
def setup
@handler = ConnectionHandler.new
@spec_name = "primary"
@@ -66,6 +71,56 @@ module ActiveRecord
ENV["RAILS_ENV"] = previous_env
end
+ unless in_memory_db?
+ def test_establish_connection_using_3_level_config_defaults_to_default_env_primary_db
+ previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
+
+ config = {
+ "default_env" => {
+ "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" },
+ "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }
+ },
+ "another_env" => {
+ "primary" => { "adapter" => "sqlite3", "database" => "db/another-primary.sqlite3" },
+ "readonly" => { "adapter" => "sqlite3", "database" => "db/another-readonly.sqlite3" }
+ }
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ ActiveRecord::Base.establish_connection
+
+ assert_match "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ENV["RAILS_ENV"] = previous_env
+ ActiveRecord::Base.establish_connection(:arunit)
+ FileUtils.rm_rf "db"
+ end
+
+ def test_establish_connection_using_2_level_config_defaults_to_default_env_primary_db
+ previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env"
+
+ config = {
+ "default_env" => {
+ "adapter" => "sqlite3", "database" => "db/primary.sqlite3"
+ },
+ "another_env" => {
+ "adapter" => "sqlite3", "database" => "db/bad-primary.sqlite3"
+ }
+ }
+ @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
+
+ ActiveRecord::Base.establish_connection
+
+ assert_match "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database]
+ ensure
+ ActiveRecord::Base.configurations = @prev_configs
+ ENV["RAILS_ENV"] = previous_env
+ ActiveRecord::Base.establish_connection(:arunit)
+ FileUtils.rm_rf "db"
+ end
+ end
+
def test_establish_connection_using_two_level_configurations
config = { "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } }
@prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config
@@ -98,11 +153,11 @@ module ActiveRecord
end
def test_active_connections?
- assert !@handler.active_connections?
+ assert_not_predicate @handler, :active_connections?
assert @handler.retrieve_connection(@spec_name)
- assert @handler.active_connections?
+ assert_predicate @handler, :active_connections?
@handler.clear_active_connections!
- assert !@handler.active_connections?
+ assert_not_predicate @handler, :active_connections?
end
def test_retrieve_connection_pool
@@ -139,6 +194,75 @@ module ActiveRecord
rd.close
end
+ def test_forked_child_doesnt_mangle_parent_connection
+ object_id = ActiveRecord::Base.connection.object_id
+ assert_predicate ActiveRecord::Base.connection, :active?
+
+ rd, wr = IO.pipe
+ rd.binmode
+ wr.binmode
+
+ pid = fork {
+ rd.close
+ if ActiveRecord::Base.connection.active?
+ wr.write Marshal.dump ActiveRecord::Base.connection.object_id
+ end
+ wr.close
+
+ exit # allow finalizers to run
+ }
+
+ wr.close
+
+ Process.waitpid pid
+ assert_not_equal object_id, Marshal.load(rd.read)
+ rd.close
+
+ assert_equal 3, ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM people")
+ end
+
+ unless in_memory_db?
+ def test_forked_child_recovers_from_disconnected_parent
+ object_id = ActiveRecord::Base.connection.object_id
+ assert_predicate ActiveRecord::Base.connection, :active?
+
+ rd, wr = IO.pipe
+ rd.binmode
+ wr.binmode
+
+ outer_pid = fork {
+ ActiveRecord::Base.connection.disconnect!
+
+ pid = fork {
+ rd.close
+ if ActiveRecord::Base.connection.active?
+ pair = [ActiveRecord::Base.connection.object_id,
+ ActiveRecord::Base.connection.select_value("SELECT COUNT(*) FROM people")]
+ wr.write Marshal.dump pair
+ end
+ wr.close
+
+ exit # allow finalizers to run
+ }
+
+ Process.waitpid pid
+ }
+
+ wr.close
+
+ Process.waitpid outer_pid
+ child_id, child_count = Marshal.load(rd.read)
+
+ assert_not_equal object_id, child_id
+ rd.close
+
+ assert_equal 3, child_count
+
+ # Outer connection is unaffected
+ assert_equal 6, ActiveRecord::Base.connection.select_value("SELECT 2 * COUNT(*) FROM people")
+ end
+ end
+
def test_retrieve_connection_pool_copies_schema_cache_from_ancestor_pool
@pool.schema_cache = @pool.connection.schema_cache
@pool.schema_cache.add("posts")
@@ -200,15 +324,15 @@ module ActiveRecord
def test_a_class_using_custom_pool_and_switching_back_to_primary
klass2 = Class.new(Base) { def self.name; "klass2"; end }
- assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id
+ assert_same klass2.connection, ActiveRecord::Base.connection
pool = klass2.establish_connection(ActiveRecord::Base.connection_pool.spec.config)
- assert_equal klass2.connection.object_id, pool.connection.object_id
- refute_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id
+ assert_same klass2.connection, pool.connection
+ assert_not_same klass2.connection, ActiveRecord::Base.connection
klass2.remove_connection
- assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id
+ assert_same klass2.connection, ActiveRecord::Base.connection
end
def test_connection_specification_name_should_fallback_to_parent
@@ -223,8 +347,8 @@ module ActiveRecord
def test_remove_connection_should_not_remove_parent
klass2 = Class.new(Base) { def self.name; "klass2"; end }
klass2.remove_connection
- refute_nil ActiveRecord::Base.connection.object_id
- assert_equal klass2.connection.object_id, ActiveRecord::Base.connection.object_id
+ assert_not_nil ActiveRecord::Base.connection
+ assert_same klass2.connection, ActiveRecord::Base.connection
end
end
end
diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
index 006be9e65d..67496381d1 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -22,8 +22,8 @@ module ActiveRecord
new_cache = YAML.load(YAML.dump(@cache))
assert_no_queries do
- assert_equal 11, new_cache.columns("posts").size
- assert_equal 11, new_cache.columns_hash("posts").size
+ assert_equal 12, new_cache.columns("posts").size
+ assert_equal 12, new_cache.columns_hash("posts").size
assert new_cache.data_sources("posts")
assert_equal "id", new_cache.primary_keys("posts")
end
@@ -75,8 +75,8 @@ module ActiveRecord
@cache = Marshal.load(Marshal.dump(@cache))
assert_no_queries do
- assert_equal 11, @cache.columns("posts").size
- assert_equal 11, @cache.columns_hash("posts").size
+ assert_equal 12, @cache.columns("posts").size
+ assert_equal 12, @cache.columns_hash("posts").size
assert @cache.data_sources("posts")
assert_equal "id", @cache.primary_keys("posts")
end
diff --git a/activerecord/test/cases/connection_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
index 917a04ebc3..1c79d776f0 100644
--- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb
+++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb
@@ -82,11 +82,11 @@ unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strin
end
def test_bigint_limit
- cast_type = @connection.send(:type_map).lookup("bigint")
+ limit = @connection.send(:type_map).lookup("bigint").send(:_limit)
if current_adapter?(:OracleAdapter)
- assert_equal 19, cast_type.limit
+ assert_equal 19, limit
else
- assert_equal 8, cast_type.limit
+ assert_equal 8, limit
end
end
diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb
index 9d6ecbde78..0941ee3309 100644
--- a/activerecord/test/cases/connection_management_test.rb
+++ b/activerecord/test/cases/connection_management_test.rb
@@ -27,7 +27,7 @@ module ActiveRecord
# make sure we have an active connection
assert ActiveRecord::Base.connection
- assert ActiveRecord::Base.connection_handler.active_connections?
+ assert_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
def test_app_delegation
@@ -47,14 +47,14 @@ module ActiveRecord
def test_connections_are_cleared_after_body_close
_, _, body = @management.call(@env)
body.close
- assert !ActiveRecord::Base.connection_handler.active_connections?
+ assert_not_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
def test_active_connections_are_not_cleared_on_body_close_during_transaction
ActiveRecord::Base.transaction do
_, _, body = @management.call(@env)
body.close
- assert ActiveRecord::Base.connection_handler.active_connections?
+ assert_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
end
@@ -62,7 +62,7 @@ module ActiveRecord
app = Class.new(App) { def call(env); raise NotImplementedError; end }.new
explosive = middleware(app)
assert_raises(NotImplementedError) { explosive.call(@env) }
- assert !ActiveRecord::Base.connection_handler.active_connections?
+ assert_not_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
def test_connections_not_closed_if_exception_inside_transaction
@@ -70,14 +70,14 @@ module ActiveRecord
app = Class.new(App) { def call(env); raise RuntimeError; end }.new
explosive = middleware(app)
assert_raises(RuntimeError) { explosive.call(@env) }
- assert ActiveRecord::Base.connection_handler.active_connections?
+ assert_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
end
test "doesn't clear active connections when running in a test case" do
executor.wrap do
@management.call(@env)
- assert ActiveRecord::Base.connection_handler.active_connections?
+ assert_predicate ActiveRecord::Base.connection_handler, :active_connections?
end
end
@@ -85,7 +85,7 @@ module ActiveRecord
body = Class.new(String) { def to_path; "/path"; end }.new
app = lambda { |_| [200, {}, body] }
response_body = middleware(app).call(@env)[2]
- assert response_body.respond_to?(:to_path)
+ assert_respond_to response_body, :to_path
assert_equal "/path", response_body.to_path
end
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
index 2bfe490602..9ac03629c3 100644
--- a/activerecord/test/cases/connection_pool_test.rb
+++ b/activerecord/test/cases/connection_pool_test.rb
@@ -35,12 +35,12 @@ module ActiveRecord
def test_checkout_after_close
connection = pool.connection
- assert connection.in_use?
+ assert_predicate connection, :in_use?
connection.close
- assert !connection.in_use?
+ assert_not_predicate connection, :in_use?
- assert pool.connection.in_use?
+ assert_predicate pool.connection, :in_use?
end
def test_released_connection_moves_between_threads
@@ -80,18 +80,18 @@ module ActiveRecord
end
def test_active_connection_in_use
- assert !pool.active_connection?
+ assert_not_predicate pool, :active_connection?
main_thread = pool.connection
- assert pool.active_connection?
+ assert_predicate pool, :active_connection?
main_thread.close
- assert !pool.active_connection?
+ assert_not_predicate pool, :active_connection?
end
def test_full_pool_exception
- @pool.size.times { @pool.checkout }
+ @pool.size.times { assert @pool.checkout }
assert_raises(ConnectionTimeoutError) do
@pool.checkout
end
@@ -156,13 +156,60 @@ module ActiveRecord
@pool.connections.each { |conn| conn.close if conn.in_use? }
end
+ def test_flush
+ idle_conn = @pool.checkout
+ recent_conn = @pool.checkout
+ active_conn = @pool.checkout
+
+ @pool.checkin idle_conn
+ @pool.checkin recent_conn
+
+ assert_equal 3, @pool.connections.length
+
+ def idle_conn.seconds_idle
+ 1000
+ end
+
+ @pool.flush(30)
+
+ assert_equal 2, @pool.connections.length
+
+ assert_equal [recent_conn, active_conn].sort_by(&:__id__), @pool.connections.sort_by(&:__id__)
+ ensure
+ @pool.checkin active_conn
+ end
+
+ def test_flush_bang
+ idle_conn = @pool.checkout
+ recent_conn = @pool.checkout
+ active_conn = @pool.checkout
+ _dead_conn = Thread.new { @pool.checkout }.join
+
+ @pool.checkin idle_conn
+ @pool.checkin recent_conn
+
+ assert_equal 4, @pool.connections.length
+
+ def idle_conn.seconds_idle
+ 1000
+ end
+
+ @pool.flush!
+
+ assert_equal 1, @pool.connections.length
+
+ assert_equal [active_conn].sort_by(&:__id__), @pool.connections.sort_by(&:__id__)
+ ensure
+ @pool.checkin active_conn
+ end
+
def test_remove_connection
conn = @pool.checkout
- assert conn.in_use?
+ assert_predicate conn, :in_use?
length = @pool.connections.length
@pool.remove conn
- assert conn.in_use?
+ assert_predicate conn, :in_use?
assert_equal(length - 1, @pool.connections.length)
ensure
conn.close
@@ -177,11 +224,11 @@ module ActiveRecord
end
def test_active_connection?
- assert !@pool.active_connection?
+ assert_not_predicate @pool, :active_connection?
assert @pool.connection
- assert @pool.active_connection?
+ assert_predicate @pool, :active_connection?
@pool.release_connection
- assert !@pool.active_connection?
+ assert_not_predicate @pool, :active_connection?
end
def test_checkout_behaviour
@@ -205,6 +252,14 @@ module ActiveRecord
end.join
end
+ def test_checkout_order_is_lifo
+ conn1 = @pool.checkout
+ conn2 = @pool.checkout
+ @pool.checkin conn1
+ @pool.checkin conn2
+ assert_equal [conn2, conn1], 2.times.map { @pool.checkout }
+ end
+
# The connection pool is "fair" if threads waiting for
# connections receive them in the order in which they began
# waiting. This ensures that we don't timeout one HTTP request
@@ -414,6 +469,7 @@ module ActiveRecord
end
def test_non_bang_disconnect_and_clear_reloadable_connections_throw_exception_if_threads_dont_return_their_conns
+ Thread.report_on_exception, original_report_on_exception = false, Thread.report_on_exception
@pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout
[:disconnect, :clear_reloadable_connections].each do |group_action_method|
@pool.with_connection do |connection|
@@ -422,6 +478,8 @@ module ActiveRecord
end
end
end
+ ensure
+ Thread.report_on_exception = original_report_on_exception
end
def test_disconnect_and_clear_reloadable_connections_attempt_to_wait_for_threads_to_return_their_conns
@@ -438,7 +496,7 @@ module ActiveRecord
assert_nil timed_join_result
# assert that since this is within default timeout our connection hasn't been forcefully taken away from us
- assert @pool.active_connection?
+ assert_predicate @pool, :active_connection?
end
ensure
thread.join if thread && !timed_join_result # clean up the other thread
@@ -452,7 +510,7 @@ module ActiveRecord
@pool.with_connection do |connection|
Thread.new { @pool.send(group_action_method) }.join
# assert connection has been forcefully taken away from us
- assert_not @pool.active_connection?
+ assert_not_predicate @pool, :active_connection?
# make a new connection for with_connection to clean up
@pool.connection
diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb
index 3fa0ca8366..5b80f16a44 100644
--- a/activerecord/test/cases/connection_specification/resolver_test.rb
+++ b/activerecord/test/cases/connection_specification/resolver_test.rb
@@ -19,7 +19,7 @@ module ActiveRecord
spec "ridiculous://foo?encoding=utf8"
end
- assert_match "Could not load 'active_record/connection_adapters/ridiculous_adapter'", error.message
+ assert_match "Could not load the 'ridiculous' Active Record adapter. Ensure that the adapter is spelled correctly in config/database.yml and that you've added the necessary adapter gem to your Gemfile.", error.message
end
# The abstract adapter is used simply to bypass the bit of code that
diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb
index 356afdbd2b..6e7ae2efb4 100644
--- a/activerecord/test/cases/core_test.rb
+++ b/activerecord/test/cases/core_test.rb
@@ -4,7 +4,6 @@ require "cases/helper"
require "models/person"
require "models/topic"
require "pp"
-require "active_support/core_ext/string/strip"
class NonExistentTable < ActiveRecord::Base; end
@@ -39,26 +38,26 @@ class CoreTest < ActiveRecord::TestCase
topic = Topic.new
actual = "".dup
PP.pp(topic, StringIO.new(actual))
- expected = <<-PRETTY.strip_heredoc
- #<Topic:0xXXXXXX
- id: nil,
- title: nil,
- author_name: nil,
- author_email_address: "test@test.com",
- written_on: nil,
- bonus_time: nil,
- last_read: nil,
- content: nil,
- important: nil,
- approved: true,
- replies_count: 0,
- unique_replies_count: 0,
- parent_id: nil,
- parent_title: nil,
- type: nil,
- group: nil,
- created_at: nil,
- updated_at: nil>
+ expected = <<~PRETTY
+ #<Topic:0xXXXXXX
+ id: nil,
+ title: nil,
+ author_name: nil,
+ author_email_address: "test@test.com",
+ written_on: nil,
+ bonus_time: nil,
+ last_read: nil,
+ content: nil,
+ important: nil,
+ approved: true,
+ replies_count: 0,
+ unique_replies_count: 0,
+ parent_id: nil,
+ parent_title: nil,
+ type: nil,
+ group: nil,
+ created_at: nil,
+ updated_at: nil>
PRETTY
assert actual.start_with?(expected.split("XXXXXX").first)
assert actual.end_with?(expected.split("XXXXXX").last)
@@ -68,26 +67,26 @@ class CoreTest < ActiveRecord::TestCase
topic = topics(:first)
actual = "".dup
PP.pp(topic, StringIO.new(actual))
- expected = <<-PRETTY.strip_heredoc
- #<Topic:0x\\w+
- id: 1,
- title: "The First Topic",
- author_name: "David",
- author_email_address: "david@loudthinking.com",
- written_on: 2003-07-16 14:28:11 UTC,
- bonus_time: 2000-01-01 14:28:00 UTC,
- last_read: Thu, 15 Apr 2004,
- content: "Have a nice day",
- important: nil,
- approved: false,
- replies_count: 1,
- unique_replies_count: 0,
- parent_id: nil,
- parent_title: nil,
- type: nil,
- group: nil,
- created_at: [^,]+,
- updated_at: [^,>]+>
+ expected = <<~PRETTY
+ #<Topic:0x\\w+
+ id: 1,
+ title: "The First Topic",
+ author_name: "David",
+ author_email_address: "david@loudthinking.com",
+ written_on: 2003-07-16 14:28:11 UTC,
+ bonus_time: 2000-01-01 14:28:00 UTC,
+ last_read: Thu, 15 Apr 2004,
+ content: "Have a nice day",
+ important: nil,
+ approved: false,
+ replies_count: 1,
+ unique_replies_count: 0,
+ parent_id: nil,
+ parent_title: nil,
+ type: nil,
+ group: nil,
+ created_at: [^,]+,
+ updated_at: [^,>]+>
PRETTY
assert_match(/\A#{expected}\z/, actual)
end
diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb
index 51f6164138..e64a8372d0 100644
--- a/activerecord/test/cases/date_time_precision_test.rb
+++ b/activerecord/test/cases/date_time_precision_test.rb
@@ -27,6 +27,24 @@ if subsecond_precision_supported?
assert_equal 5, Foo.columns_hash["updated_at"].precision
end
+ 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, :updated_at, :datetime, precision: 6
+
+ time = ::Time.now.change(nsec: 123456789)
+ foo = Foo.new(created_at: time, updated_at: time)
+
+ assert_equal 0, foo.created_at.nsec
+ assert_equal 123456000, foo.updated_at.nsec
+
+ foo.save!
+ foo.reload
+
+ assert_equal 0, foo.created_at.nsec
+ assert_equal 123456000, foo.updated_at.nsec
+ end
+
def test_timestamps_helper_with_custom_precision
@connection.create_table(:foos, force: true) do |t|
t.timestamps precision: 4
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index 4690682cd8..3d11b573f1 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -53,7 +53,7 @@ class DefaultNumbersTest < ActiveRecord::TestCase
def test_default_decimal_number
record = DefaultNumber.new
- assert_equal BigDecimal.new("2.78"), record.decimal_number
+ assert_equal BigDecimal("2.78"), record.decimal_number
assert_equal "2.78", record.decimal_number_before_type_cast
end
end
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index a602f83d8c..83cc2aa319 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -24,18 +24,18 @@ class DirtyTest < ActiveRecord::TestCase
# Change catchphrase.
pirate.catchphrase = "arrr"
- assert pirate.catchphrase_changed?
+ assert_predicate pirate, :catchphrase_changed?
assert_nil pirate.catchphrase_was
assert_equal [nil, "arrr"], pirate.catchphrase_change
# Saved - no changes.
pirate.save!
- assert !pirate.catchphrase_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
assert_nil pirate.catchphrase_change
# Same value - no changes.
pirate.catchphrase = "arrr"
- assert !pirate.catchphrase_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
assert_nil pirate.catchphrase_change
end
@@ -46,23 +46,23 @@ class DirtyTest < ActiveRecord::TestCase
# New record - no changes.
pirate = target.new
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Saved - no changes.
pirate.catchphrase = "arrrr, time zone!!"
pirate.save!
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Change created_on.
old_created_on = pirate.created_on
pirate.created_on = Time.now - 1.day
- assert pirate.created_on_changed?
+ assert_predicate pirate, :created_on_changed?
assert_kind_of ActiveSupport::TimeWithZone, pirate.created_on_was
assert_equal old_created_on, pirate.created_on_was
pirate.created_on = old_created_on
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
end
end
@@ -73,7 +73,7 @@ class DirtyTest < ActiveRecord::TestCase
pirate = target.create!
pirate.created_on = pirate.created_on
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
end
end
@@ -86,19 +86,19 @@ class DirtyTest < ActiveRecord::TestCase
# New record - no changes.
pirate = target.new
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Saved - no changes.
pirate.catchphrase = "arrrr, time zone!!"
pirate.save!
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Change created_on.
old_created_on = pirate.created_on
pirate.created_on = Time.now + 1.day
- assert pirate.created_on_changed?
+ assert_predicate pirate, :created_on_changed?
# kind_of does not work because
# ActiveSupport::TimeWithZone.name == 'Time'
assert_instance_of Time, pirate.created_on_was
@@ -113,19 +113,19 @@ class DirtyTest < ActiveRecord::TestCase
# New record - no changes.
pirate = target.new
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Saved - no changes.
pirate.catchphrase = "arrrr, time zone!!"
pirate.save!
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
assert_nil pirate.created_on_change
# Change created_on.
old_created_on = pirate.created_on
pirate.created_on = Time.now + 1.day
- assert pirate.created_on_changed?
+ assert_predicate pirate, :created_on_changed?
# kind_of does not work because
# ActiveSupport::TimeWithZone.name == 'Time'
assert_instance_of Time, pirate.created_on_was
@@ -137,11 +137,11 @@ class DirtyTest < ActiveRecord::TestCase
# the actual attribute here is name, title is an
# alias setup via alias_attribute
parrot = Parrot.new
- assert !parrot.title_changed?
+ assert_not_predicate parrot, :title_changed?
assert_nil parrot.title_change
parrot.name = "Sam"
- assert parrot.title_changed?
+ assert_predicate parrot, :title_changed?
assert_nil parrot.title_was
assert_equal parrot.name_change, parrot.title_change
end
@@ -153,7 +153,7 @@ class DirtyTest < ActiveRecord::TestCase
pirate.restore_catchphrase!
assert_equal "Yar!", pirate.catchphrase
assert_equal Hash.new, pirate.changes
- assert !pirate.catchphrase_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
end
def test_nullable_number_not_marked_as_changed_if_new_value_is_blank
@@ -161,7 +161,7 @@ class DirtyTest < ActiveRecord::TestCase
["", nil].each do |value|
pirate.parrot_id = value
- assert !pirate.parrot_id_changed?
+ assert_not_predicate pirate, :parrot_id_changed?
assert_nil pirate.parrot_id_change
end
end
@@ -171,7 +171,7 @@ class DirtyTest < ActiveRecord::TestCase
["", nil].each do |value|
numeric_data.bank_balance = value
- assert !numeric_data.bank_balance_changed?
+ assert_not_predicate numeric_data, :bank_balance_changed?
assert_nil numeric_data.bank_balance_change
end
end
@@ -181,7 +181,7 @@ class DirtyTest < ActiveRecord::TestCase
["", nil].each do |value|
numeric_data.temperature = value
- assert !numeric_data.temperature_changed?
+ assert_not_predicate numeric_data, :temperature_changed?
assert_nil numeric_data.temperature_change
end
end
@@ -197,7 +197,7 @@ class DirtyTest < ActiveRecord::TestCase
["", nil].each do |value|
topic.written_on = value
assert_nil topic.written_on
- assert !topic.written_on_changed?
+ assert_not_predicate topic, :written_on_changed?
end
end
end
@@ -208,10 +208,10 @@ class DirtyTest < ActiveRecord::TestCase
pirate.catchphrase = "arrr"
assert pirate.save!
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
pirate.parrot_id = "0"
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
end
def test_integer_zero_to_integer_zero_not_marked_as_changed
@@ -220,17 +220,17 @@ class DirtyTest < ActiveRecord::TestCase
pirate.catchphrase = "arrr"
assert pirate.save!
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
pirate.parrot_id = 0
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
end
def test_float_zero_to_string_zero_not_marked_as_changed
data = NumericData.new temperature: 0.0
data.save!
- assert_not data.changed?
+ assert_not_predicate data, :changed?
data.temperature = "0"
assert_empty data.changes
@@ -251,38 +251,38 @@ class DirtyTest < ActiveRecord::TestCase
# check the change from 1 to ''
pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties")
pirate.parrot_id = ""
- assert pirate.parrot_id_changed?
+ assert_predicate pirate, :parrot_id_changed?
assert_equal([1, nil], pirate.parrot_id_change)
pirate.save
# check the change from nil to 0
pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties")
pirate.parrot_id = 0
- assert pirate.parrot_id_changed?
+ assert_predicate pirate, :parrot_id_changed?
assert_equal([nil, 0], pirate.parrot_id_change)
pirate.save
# check the change from 0 to ''
pirate = Pirate.find_by_catchphrase("Yarrrr, me hearties")
pirate.parrot_id = ""
- assert pirate.parrot_id_changed?
+ assert_predicate pirate, :parrot_id_changed?
assert_equal([0, nil], pirate.parrot_id_change)
end
def test_object_should_be_changed_if_any_attribute_is_changed
pirate = Pirate.new
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
assert_equal [], pirate.changed
assert_equal Hash.new, pirate.changes
pirate.catchphrase = "arrr"
- assert pirate.changed?
+ assert_predicate pirate, :changed?
assert_nil pirate.catchphrase_was
assert_equal %w(catchphrase), pirate.changed
assert_equal({ "catchphrase" => [nil, "arrr"] }, pirate.changes)
pirate.save
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
assert_equal [], pirate.changed
assert_equal Hash.new, pirate.changes
end
@@ -290,40 +290,40 @@ class DirtyTest < ActiveRecord::TestCase
def test_attribute_will_change!
pirate = Pirate.create!(catchphrase: "arr")
- assert !pirate.catchphrase_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
assert pirate.catchphrase_will_change!
- assert pirate.catchphrase_changed?
+ assert_predicate pirate, :catchphrase_changed?
assert_equal ["arr", "arr"], pirate.catchphrase_change
pirate.catchphrase << " matey!"
- assert pirate.catchphrase_changed?
+ assert_predicate pirate, :catchphrase_changed?
assert_equal ["arr", "arr matey!"], pirate.catchphrase_change
end
def test_virtual_attribute_will_change
parrot = Parrot.create!(name: "Ruby")
parrot.send(:attribute_will_change!, :cancel_save_from_callback)
- assert parrot.has_changes_to_save?
+ assert_predicate parrot, :has_changes_to_save?
end
def test_association_assignment_changes_foreign_key
pirate = Pirate.create!(catchphrase: "jarl")
pirate.parrot = Parrot.create!(name: "Lorre")
- assert pirate.changed?
+ assert_predicate pirate, :changed?
assert_equal %w(parrot_id), pirate.changed
end
def test_attribute_should_be_compared_with_type_cast
topic = Topic.new
- assert topic.approved?
- assert !topic.approved_changed?
+ assert_predicate topic, :approved?
+ assert_not_predicate topic, :approved_changed?
# Coming from web form.
params = { topic: { approved: 1 } }
# In the controller.
topic.attributes = params[:topic]
- assert topic.approved?
- assert !topic.approved_changed?
+ assert_predicate topic, :approved?
+ assert_not_predicate topic, :approved_changed?
end
def test_partial_update
@@ -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
@@ -378,9 +378,9 @@ class DirtyTest < ActiveRecord::TestCase
def test_reload_should_clear_changed_attributes
pirate = Pirate.create!(catchphrase: "shiver me timbers")
pirate.catchphrase = "*hic*"
- assert pirate.changed?
+ assert_predicate pirate, :changed?
pirate.reload
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
end
def test_dup_objects_should_not_copy_dirty_flag_from_creator
@@ -388,17 +388,17 @@ class DirtyTest < ActiveRecord::TestCase
pirate_dup = pirate.dup
pirate_dup.restore_catchphrase!
pirate.catchphrase = "I love Rum"
- assert pirate.catchphrase_changed?
- assert !pirate_dup.catchphrase_changed?
+ assert_predicate pirate, :catchphrase_changed?
+ assert_not_predicate pirate_dup, :catchphrase_changed?
end
def test_reverted_changes_are_not_dirty
phrase = "shiver me timbers"
pirate = Pirate.create!(catchphrase: phrase)
pirate.catchphrase = "*hic*"
- assert pirate.changed?
+ assert_predicate pirate, :changed?
pirate.catchphrase = phrase
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
end
def test_reverted_changes_are_not_dirty_after_multiple_changes
@@ -406,40 +406,40 @@ class DirtyTest < ActiveRecord::TestCase
pirate = Pirate.create!(catchphrase: phrase)
10.times do |i|
pirate.catchphrase = "*hic*" * i
- assert pirate.changed?
+ assert_predicate pirate, :changed?
end
- assert pirate.changed?
+ assert_predicate pirate, :changed?
pirate.catchphrase = phrase
- assert !pirate.changed?
+ assert_not_predicate pirate, :changed?
end
def test_reverted_changes_are_not_dirty_going_from_nil_to_value_and_back
pirate = Pirate.create!(catchphrase: "Yar!")
pirate.parrot_id = 1
- assert pirate.changed?
- assert pirate.parrot_id_changed?
- assert !pirate.catchphrase_changed?
+ assert_predicate pirate, :changed?
+ assert_predicate pirate, :parrot_id_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
pirate.parrot_id = nil
- assert !pirate.changed?
- assert !pirate.parrot_id_changed?
- assert !pirate.catchphrase_changed?
+ assert_not_predicate pirate, :changed?
+ assert_not_predicate pirate, :parrot_id_changed?
+ assert_not_predicate pirate, :catchphrase_changed?
end
def test_save_should_store_serialized_attributes_even_with_partial_writes
with_partial_writes(Topic) do
topic = Topic.create!(content: { a: "a" })
- assert_not topic.changed?
+ assert_not_predicate topic, :changed?
topic.content[:b] = "b"
- assert topic.changed?
+ assert_predicate topic, :changed?
topic.save!
- assert_not topic.changed?
+ assert_not_predicate topic, :changed?
assert_equal "b", topic.content[:b]
topic.reload
@@ -473,6 +473,14 @@ class DirtyTest < ActiveRecord::TestCase
end
end
+ def test_changes_to_save_should_not_mutate_array_of_hashes
+ topic = Topic.new(author_name: "Bill", content: [{ a: "a" }])
+
+ topic.changes_to_save
+
+ assert_equal [{ a: "a" }], topic.content
+ end
+
def test_previous_changes
# original values should be in previous_changes
pirate = Pirate.new
@@ -488,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
@@ -502,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
@@ -519,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!")
@@ -533,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)
@@ -545,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)
@@ -557,8 +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")
+ assert_not pirate.previous_changes.key?("parrot_id")
+ assert_not pirate.previous_changes.key?("created_on")
ensure
travel_back
end
@@ -596,7 +604,7 @@ class DirtyTest < ActiveRecord::TestCase
pirate = Pirate.create!(catchphrase: "rrrr", created_on: time_in_paris)
pirate.created_on = pirate.created_on.in_time_zone("Tokyo").to_s
- assert !pirate.created_on_changed?
+ assert_not_predicate pirate, :created_on_changed?
end
test "partial insert" do
@@ -627,7 +635,7 @@ class DirtyTest < ActiveRecord::TestCase
pirate = Pirate.create!(catchphrase: "arrrr")
pirate.catchphrase << " matey!"
- assert pirate.catchphrase_changed?
+ assert_predicate pirate, :catchphrase_changed?
expected_changes = {
"catchphrase" => ["arrrr", "arrrr matey!"]
}
@@ -641,7 +649,7 @@ class DirtyTest < ActiveRecord::TestCase
pirate.reload
assert_equal "arrrr matey!", pirate.catchphrase
- assert_not pirate.changed?
+ assert_not_predicate pirate, :changed?
end
test "in place mutation for binary" do
@@ -652,19 +660,19 @@ class DirtyTest < ActiveRecord::TestCase
binary = klass.create!(data: "\\\\foo")
- assert_not binary.changed?
+ assert_not_predicate binary, :changed?
binary.data = binary.data.dup
- assert_not binary.changed?
+ assert_not_predicate binary, :changed?
binary = klass.last
- assert_not binary.changed?
+ assert_not_predicate binary, :changed?
binary.data << "bar"
- assert binary.changed?
+ assert_predicate binary, :changed?
end
test "changes is correct for subclass" do
@@ -679,7 +687,7 @@ class DirtyTest < ActiveRecord::TestCase
new_catchphrase = "arrrr matey!"
pirate.catchphrase = new_catchphrase
- assert pirate.catchphrase_changed?
+ assert_predicate pirate, :catchphrase_changed?
expected_changes = {
"catchphrase" => ["arrrr", new_catchphrase]
@@ -698,7 +706,7 @@ class DirtyTest < ActiveRecord::TestCase
new_catchphrase = "arrrr matey!"
pirate.catchphrase = new_catchphrase
- assert pirate.catchphrase_changed?
+ assert_predicate pirate, :catchphrase_changed?
expected_changes = {
"catchphrase" => ["arrrr", new_catchphrase]
@@ -720,7 +728,7 @@ class DirtyTest < ActiveRecord::TestCase
end
model = klass.new(first_name: "Jim")
- assert model.first_name_changed?
+ assert_predicate model, :first_name_changed?
end
test "attribute_will_change! doesn't try to save non-persistable attributes" do
@@ -732,10 +740,28 @@ class DirtyTest < ActiveRecord::TestCase
record = klass.new(first_name: "Sean")
record.non_persisted_attribute_will_change!
- assert record.non_persisted_attribute_changed?
+ assert_predicate record, :non_persisted_attribute_changed?
assert record.save
end
+ test "virtual attributes are not written with partial_writes off" do
+ with_partial_writes(ActiveRecord::Base, false) do
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "people"
+ attribute :non_persisted_attribute, :string
+ end
+
+ record = klass.new(first_name: "Sean")
+ record.non_persisted_attribute_will_change!
+
+ assert record.save
+
+ record.non_persisted_attribute_will_change!
+
+ assert record.save
+ end
+ end
+
test "mutating and then assigning doesn't remove the change" do
pirate = Pirate.create!(catchphrase: "arrrr")
pirate.catchphrase << " matey!"
@@ -762,26 +788,33 @@ class DirtyTest < ActiveRecord::TestCase
test "attributes assigned but not selected are dirty" do
person = Person.select(:id).first
- refute person.changed?
+ assert_not_predicate person, :changed?
person.first_name = "Sean"
- assert person.changed?
+ assert_predicate person, :changed?
person.first_name = nil
- assert person.changed?
+ assert_predicate person, :changed?
+ end
+
+ test "attributes not selected are still missing after save" do
+ person = Person.select(:id).first
+ assert_raises(ActiveModel::MissingAttributeError) { person.first_name }
+ assert person.save # calls forget_attribute_assignments
+ assert_raises(ActiveModel::MissingAttributeError) { person.first_name }
end
test "saved_change_to_attribute? returns whether a change occurred in the last save" do
person = Person.create!(first_name: "Sean")
- assert person.saved_change_to_first_name?
- refute person.saved_change_to_gender?
+ assert_predicate person, :saved_change_to_first_name?
+ assert_not_predicate person, :saved_change_to_gender?
assert person.saved_change_to_first_name?(from: nil, to: "Sean")
assert person.saved_change_to_first_name?(from: nil)
assert person.saved_change_to_first_name?(to: "Sean")
- refute person.saved_change_to_first_name?(from: "Jim", to: "Sean")
- refute person.saved_change_to_first_name?(from: "Jim")
- refute person.saved_change_to_first_name?(to: "Jim")
+ assert_not person.saved_change_to_first_name?(from: "Jim", to: "Sean")
+ assert_not person.saved_change_to_first_name?(from: "Jim")
+ assert_not person.saved_change_to_first_name?(to: "Jim")
end
test "saved_change_to_attribute returns the change that occurred in the last save" do
@@ -816,11 +849,11 @@ class DirtyTest < ActiveRecord::TestCase
test "saved_changes? returns whether the last call to save changed anything" do
person = Person.create!(first_name: "Sean")
- assert person.saved_changes?
+ assert_predicate person, :saved_changes?
person.save
- refute person.saved_changes?
+ assert_not_predicate person, :saved_changes?
end
test "saved_changes returns a hash of all the changes that occurred" do
@@ -850,7 +883,7 @@ class DirtyTest < ActiveRecord::TestCase
end
person = klass.create!(first_name: "Sean")
- refute person.changed?
+ assert_not_predicate person, :changed?
end
private
@@ -863,8 +896,8 @@ class DirtyTest < ActiveRecord::TestCase
end
def check_pirate_after_save_failure(pirate)
- assert pirate.changed?
- assert pirate.parrot_id_changed?
+ assert_predicate pirate, :changed?
+ assert_predicate pirate, :parrot_id_changed?
assert_equal %w(parrot_id), pirate.changed
assert_nil pirate.parrot_id_was
end
diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb
index 2fefdbf204..387a0b1fdd 100644
--- a/activerecord/test/cases/dup_test.rb
+++ b/activerecord/test/cases/dup_test.rb
@@ -3,13 +3,14 @@
require "cases/helper"
require "models/reply"
require "models/topic"
+require "models/movie"
module ActiveRecord
class DupTest < ActiveRecord::TestCase
fixtures :topics
def test_dup
- assert !Topic.new.freeze.dup.frozen?
+ assert_not_predicate Topic.new.freeze.dup, :frozen?
end
def test_not_readonly
@@ -40,7 +41,7 @@ module ActiveRecord
topic.destroy
duped = topic.dup
- assert_not duped.destroyed?
+ assert_not_predicate duped, :destroyed?
end
def test_dup_has_no_id
@@ -62,10 +63,10 @@ module ActiveRecord
topic.attributes = dbtopic.attributes.except("id")
- #duped has no timestamp values
+ # duped has no timestamp values
duped = dbtopic.dup
- #clear topic timestamp values
+ # clear topic timestamp values
topic.send(:clear_timestamp_attributes)
assert_equal topic.changes, duped.changes
@@ -100,7 +101,7 @@ module ActiveRecord
# temporary change to the topic object
topic.updated_at -= 3.days
- #dup should not preserve the timestamps if present
+ # dup should not preserve the timestamps if present
new_topic = topic.dup
assert_nil new_topic.updated_at
assert_nil new_topic.created_at
@@ -126,12 +127,12 @@ module ActiveRecord
duped = topic.dup
duped.title = nil
- assert duped.invalid?
+ assert_predicate duped, :invalid?
topic.title = nil
duped.title = "Mathematics"
- assert topic.invalid?
- assert duped.valid?
+ assert_predicate topic, :invalid?
+ assert_predicate duped, :valid?
end
end
@@ -157,5 +158,20 @@ module ActiveRecord
record.dup
end
end
+
+ def test_dup_record_not_persisted_after_rollback_transaction
+ movie = Movie.new(name: "test")
+
+ assert_raises(ActiveRecord::RecordInvalid) do
+ Movie.transaction do
+ movie.save!
+ duped = movie.dup
+ duped.name = nil
+ duped.save!
+ end
+ end
+
+ assert_not movie.persisted?
+ end
end
end
diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb
index 78cb89ccc5..d5a1d11e12 100644
--- a/activerecord/test/cases/enum_test.rb
+++ b/activerecord/test/cases/enum_test.rb
@@ -12,16 +12,16 @@ class EnumTest < ActiveRecord::TestCase
end
test "query state by predicate" do
- assert @book.published?
- assert_not @book.written?
- assert_not @book.proposed?
+ assert_predicate @book, :published?
+ assert_not_predicate @book, :written?
+ assert_not_predicate @book, :proposed?
- assert @book.read?
- assert @book.in_english?
- assert @book.author_visibility_visible?
- assert @book.illustrator_visibility_visible?
- assert @book.with_medium_font_size?
- assert @book.medium_to_read?
+ assert_predicate @book, :read?
+ assert_predicate @book, :in_english?
+ assert_predicate @book, :author_visibility_visible?
+ assert_predicate @book, :illustrator_visibility_visible?
+ assert_predicate @book, :with_medium_font_size?
+ assert_predicate @book, :medium_to_read?
end
test "query state with strings" do
@@ -76,46 +76,46 @@ class EnumTest < ActiveRecord::TestCase
end
test "build from scope" do
- assert Book.written.build.written?
- assert_not Book.written.build.proposed?
+ assert_predicate Book.written.build, :written?
+ assert_not_predicate Book.written.build, :proposed?
end
test "build from where" do
- assert Book.where(status: Book.statuses[:written]).build.written?
- assert_not Book.where(status: Book.statuses[:written]).build.proposed?
- assert Book.where(status: :written).build.written?
- assert_not Book.where(status: :written).build.proposed?
- assert Book.where(status: "written").build.written?
- assert_not Book.where(status: "written").build.proposed?
+ assert_predicate Book.where(status: Book.statuses[:written]).build, :written?
+ assert_not_predicate Book.where(status: Book.statuses[:written]).build, :proposed?
+ assert_predicate Book.where(status: :written).build, :written?
+ assert_not_predicate Book.where(status: :written).build, :proposed?
+ assert_predicate Book.where(status: "written").build, :written?
+ assert_not_predicate Book.where(status: "written").build, :proposed?
end
test "update by declaration" do
@book.written!
- assert @book.written?
+ assert_predicate @book, :written?
@book.in_english!
- assert @book.in_english?
+ assert_predicate @book, :in_english?
@book.author_visibility_visible!
- assert @book.author_visibility_visible?
+ assert_predicate @book, :author_visibility_visible?
end
test "update by setter" do
@book.update! status: :written
- assert @book.written?
+ assert_predicate @book, :written?
end
test "enum methods are overwritable" do
assert_equal "do publish work...", @book.published!
- assert @book.published?
+ assert_predicate @book, :published?
end
test "direct assignment" do
@book.status = :written
- assert @book.written?
+ assert_predicate @book, :written?
end
test "assign string value" do
@book.status = "written"
- assert @book.written?
+ assert_predicate @book, :written?
end
test "enum changed attributes" do
@@ -242,17 +242,17 @@ class EnumTest < ActiveRecord::TestCase
end
test "building new objects with enum scopes" do
- assert Book.written.build.written?
- assert Book.read.build.read?
- assert Book.in_spanish.build.in_spanish?
- assert Book.illustrator_visibility_invisible.build.illustrator_visibility_invisible?
+ assert_predicate Book.written.build, :written?
+ assert_predicate Book.read.build, :read?
+ assert_predicate Book.in_spanish.build, :in_spanish?
+ assert_predicate Book.illustrator_visibility_invisible.build, :illustrator_visibility_invisible?
end
test "creating new objects with enum scopes" do
- assert Book.written.create.written?
- assert Book.read.create.read?
- assert Book.in_spanish.create.in_spanish?
- assert Book.illustrator_visibility_invisible.create.illustrator_visibility_invisible?
+ assert_predicate Book.written.create, :written?
+ assert_predicate Book.read.create, :read?
+ assert_predicate Book.in_spanish.create, :in_spanish?
+ assert_predicate Book.illustrator_visibility_invisible.create, :illustrator_visibility_invisible?
end
test "_before_type_cast" do
@@ -308,6 +308,24 @@ class EnumTest < ActiveRecord::TestCase
end
end
+ test "reserved enum values for relation" do
+ relation_method_samples = [
+ :records,
+ :to_ary,
+ :scope_for_create
+ ]
+
+ relation_method_samples.each do |value|
+ e = assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = "books"
+ enum category: [:other, value]
+ end
+ end
+ assert_match(/You tried to define an enum named .* on the model/, e.message)
+ end
+ end
+
test "overriding enum method should not raise" do
assert_nothing_raised do
Class.new(ActiveRecord::Base) do
@@ -337,9 +355,9 @@ class EnumTest < ActiveRecord::TestCase
klass.delete_all
klass.create!(status: "proposed")
book = klass.new(status: "written")
- assert book.valid?
+ assert_predicate book, :valid?
book.status = "proposed"
- assert_not book.valid?
+ assert_not_predicate book, :valid?
end
test "validate inclusion of value in array" do
@@ -350,9 +368,9 @@ class EnumTest < ActiveRecord::TestCase
end
klass.delete_all
invalid_book = klass.new(status: "proposed")
- assert_not invalid_book.valid?
+ assert_not_predicate invalid_book, :valid?
valid_book = klass.new(status: "written")
- assert valid_book.valid?
+ assert_predicate valid_book, :valid?
end
test "enums are distinct per class" do
@@ -399,10 +417,10 @@ class EnumTest < ActiveRecord::TestCase
end
book1 = klass.proposed.create!
- assert book1.proposed?
+ assert_predicate book1, :proposed?
book2 = klass.single.create!
- assert book2.single?
+ assert_predicate book2, :single?
end
test "enum with alias_attribute" do
@@ -413,62 +431,62 @@ class EnumTest < ActiveRecord::TestCase
end
book = klass.proposed.create!
- assert book.proposed?
+ assert_predicate book, :proposed?
assert_equal "proposed", book.aliased_status
book = klass.find(book.id)
- assert book.proposed?
+ assert_predicate book, :proposed?
assert_equal "proposed", book.aliased_status
end
test "query state by predicate with prefix" do
- assert @book.author_visibility_visible?
- assert_not @book.author_visibility_invisible?
- assert @book.illustrator_visibility_visible?
- assert_not @book.illustrator_visibility_invisible?
+ assert_predicate @book, :author_visibility_visible?
+ assert_not_predicate @book, :author_visibility_invisible?
+ assert_predicate @book, :illustrator_visibility_visible?
+ assert_not_predicate @book, :illustrator_visibility_invisible?
end
test "query state by predicate with custom prefix" do
- assert @book.in_english?
- assert_not @book.in_spanish?
- assert_not @book.in_french?
+ assert_predicate @book, :in_english?
+ assert_not_predicate @book, :in_spanish?
+ assert_not_predicate @book, :in_french?
end
test "query state by predicate with custom suffix" do
- assert @book.medium_to_read?
- assert_not @book.easy_to_read?
- assert_not @book.hard_to_read?
+ assert_predicate @book, :medium_to_read?
+ assert_not_predicate @book, :easy_to_read?
+ assert_not_predicate @book, :hard_to_read?
end
test "enum methods with custom suffix defined" do
- assert @book.class.respond_to?(:easy_to_read)
- assert @book.class.respond_to?(:medium_to_read)
- assert @book.class.respond_to?(:hard_to_read)
+ assert_respond_to @book.class, :easy_to_read
+ assert_respond_to @book.class, :medium_to_read
+ assert_respond_to @book.class, :hard_to_read
- assert @book.respond_to?(:easy_to_read?)
- assert @book.respond_to?(:medium_to_read?)
- assert @book.respond_to?(:hard_to_read?)
+ assert_respond_to @book, :easy_to_read?
+ assert_respond_to @book, :medium_to_read?
+ assert_respond_to @book, :hard_to_read?
- assert @book.respond_to?(:easy_to_read!)
- assert @book.respond_to?(:medium_to_read!)
- assert @book.respond_to?(:hard_to_read!)
+ assert_respond_to @book, :easy_to_read!
+ assert_respond_to @book, :medium_to_read!
+ assert_respond_to @book, :hard_to_read!
end
test "update enum attributes with custom suffix" do
@book.medium_to_read!
- assert_not @book.easy_to_read?
- assert @book.medium_to_read?
- assert_not @book.hard_to_read?
+ assert_not_predicate @book, :easy_to_read?
+ assert_predicate @book, :medium_to_read?
+ assert_not_predicate @book, :hard_to_read?
@book.easy_to_read!
- assert @book.easy_to_read?
- assert_not @book.medium_to_read?
- assert_not @book.hard_to_read?
+ assert_predicate @book, :easy_to_read?
+ assert_not_predicate @book, :medium_to_read?
+ assert_not_predicate @book, :hard_to_read?
@book.hard_to_read!
- assert_not @book.easy_to_read?
- assert_not @book.medium_to_read?
- assert @book.hard_to_read?
+ assert_not_predicate @book, :easy_to_read?
+ assert_not_predicate @book, :medium_to_read?
+ assert_predicate @book, :hard_to_read?
end
test "uses default status when no status is provided in fixtures" do
@@ -479,12 +497,12 @@ class EnumTest < ActiveRecord::TestCase
test "uses default value from database on initialization" do
book = Book.new
- assert book.proposed?
+ assert_predicate book, :proposed?
end
test "uses default value from database on initialization when using custom mapping" do
book = Book.new
- assert book.hard?
+ assert_predicate book, :hard?
end
test "data type of Enum type" do
diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb
index fb698c47cd..82cc891970 100644
--- a/activerecord/test/cases/explain_subscriber_test.rb
+++ b/activerecord/test/cases/explain_subscriber_test.rb
@@ -15,20 +15,20 @@ if ActiveRecord::Base.connection.supports_explain?
def test_collects_nothing_if_the_payload_has_an_exception
SUBSCRIBER.finish(nil, nil, exception: Exception.new)
- assert queries.empty?
+ assert_empty queries
end
def test_collects_nothing_for_ignored_payloads
ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip|
SUBSCRIBER.finish(nil, nil, name: ip)
end
- assert queries.empty?
+ assert_empty queries
end
def test_collects_nothing_if_collect_is_false
ActiveRecord::ExplainRegistry.collect = false
SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "select 1 from users", binds: [1, 2])
- assert queries.empty?
+ assert_empty queries
end
def test_collects_pairs_of_queries_and_binds
@@ -42,12 +42,12 @@ if ActiveRecord::Base.connection.supports_explain?
def test_collects_nothing_if_the_statement_is_not_whitelisted
SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "SHOW max_identifier_length")
- assert queries.empty?
+ assert_empty queries
end
def test_collects_nothing_if_the_statement_is_only_partially_matched
SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "select_db yo_mama")
- assert queries.empty?
+ assert_empty queries
end
def test_collects_cte_queries
diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb
index 17654027a9..a0e75f4e89 100644
--- a/activerecord/test/cases/explain_test.rb
+++ b/activerecord/test/cases/explain_test.rb
@@ -2,7 +2,6 @@
require "cases/helper"
require "models/car"
-require "active_support/core_ext/string/strip"
if ActiveRecord::Base.connection.supports_explain?
class ExplainTest < ActiveRecord::TestCase
@@ -53,7 +52,7 @@ if ActiveRecord::Base.connection.supports_explain?
queries = sqls.zip(binds)
stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do
- expected = <<-SQL.strip_heredoc
+ expected = <<~SQL
EXPLAIN for: #{sqls[0]} [["wadus", 1]]
query plan foo
diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb
index 4039af66d0..59af4e6961 100644
--- a/activerecord/test/cases/finder_respond_to_test.rb
+++ b/activerecord/test/cases/finder_respond_to_test.rb
@@ -8,7 +8,7 @@ class FinderRespondToTest < ActiveRecord::TestCase
def test_should_preserve_normal_respond_to_behaviour_on_base
assert_respond_to ActiveRecord::Base, :new
- assert !ActiveRecord::Base.respond_to?(:find_by_something)
+ assert_not_respond_to ActiveRecord::Base, :find_by_something
end
def test_should_preserve_normal_respond_to_behaviour_and_respond_to_newly_added_method
@@ -43,14 +43,14 @@ class FinderRespondToTest < ActiveRecord::TestCase
end
def test_should_not_respond_to_find_by_one_missing_attribute
- assert !Topic.respond_to?(:find_by_undertitle)
+ assert_not_respond_to Topic, :find_by_undertitle
end
def test_should_not_respond_to_find_by_invalid_method_syntax
- assert !Topic.respond_to?(:fail_to_find_by_title)
- assert !Topic.respond_to?(:find_by_title?)
- assert !Topic.respond_to?(:fail_to_find_or_create_by_title)
- assert !Topic.respond_to?(:find_or_create_by_title?)
+ assert_not_respond_to Topic, :fail_to_find_by_title
+ assert_not_respond_to Topic, :find_by_title?
+ assert_not_respond_to Topic, :fail_to_find_or_create_by_title
+ assert_not_respond_to Topic, :find_or_create_by_title?
end
private
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index 55496147c1..04150f4d57 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -9,6 +9,7 @@ require "models/company"
require "models/tagging"
require "models/topic"
require "models/reply"
+require "models/rating"
require "models/entrant"
require "models/project"
require "models/developer"
@@ -119,6 +120,21 @@ class FinderTest < ActiveRecord::TestCase
assert_equal "The Fourth Topic of the day", records[2].title
end
+ def test_find_with_ids_with_no_id_passed
+ exception = assert_raises(ActiveRecord::RecordNotFound) { Topic.find }
+ assert_equal exception.model, "Topic"
+ assert_equal exception.primary_key, "id"
+ end
+
+ def test_find_with_ids_with_id_out_of_range
+ exception = assert_raises(ActiveRecord::RecordNotFound) do
+ Topic.find("9999999999999999999999999999999")
+ end
+
+ assert_equal exception.model, "Topic"
+ assert_equal exception.primary_key, "id"
+ end
+
def test_find_passing_active_record_object_is_not_permitted
assert_raises(ArgumentError) do
Topic.find(Topic.last)
@@ -156,6 +172,32 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(NoMethodError) { Topic.exists?([1, 2]) }
end
+ def test_exists_with_scope
+ davids = Author.where(name: "David")
+ assert_equal true, davids.exists?
+ assert_equal true, davids.exists?(authors(:david).id)
+ assert_equal false, davids.exists?(authors(:mary).id)
+ assert_equal false, davids.exists?("42")
+ assert_equal false, davids.exists?(42)
+ assert_equal false, davids.exists?(davids.new.id)
+
+ fake = Author.where(name: "fake author")
+ assert_equal false, fake.exists?
+ assert_equal false, fake.exists?(authors(:david).id)
+ end
+
+ def test_exists_uses_existing_scope
+ post = authors(:david).posts.first
+ authors = Author.includes(:posts).where(name: "David", posts: { id: post.id })
+ assert_equal true, authors.exists?(authors(:david).id)
+ end
+
+ def test_any_with_scope_on_hash_includes
+ post = authors(:david).posts.first
+ categories = Categorization.includes(author: :posts).where(posts: { id: post.id })
+ assert_equal true, categories.exists?
+ end
+
def test_exists_with_polymorphic_relation
post = Post.create!(title: "Post", body: "default", taggings: [Tagging.new(comment: "tagging comment")])
relation = Post.tagged_with_comment("tagging comment")
@@ -212,7 +254,7 @@ class FinderTest < ActiveRecord::TestCase
# Ensure +exists?+ runs without an error by excluding order value.
def test_exists_with_order
- assert_equal true, Topic.order("invalid sql here").exists?
+ assert_equal true, Topic.order(Arel.sql("invalid sql here")).exists?
end
def test_exists_with_joins
@@ -228,20 +270,27 @@ class FinderTest < ActiveRecord::TestCase
end
def test_exists_with_includes_limit_and_empty_result
- assert_equal false, Topic.includes(:replies).limit(0).exists?
- assert_equal false, Topic.includes(:replies).limit(1).where("0 = 1").exists?
+ assert_no_queries { assert_equal false, Topic.includes(:replies).limit(0).exists? }
+ assert_queries(1) { assert_equal false, Topic.includes(:replies).limit(1).where("0 = 1").exists? }
end
def test_exists_with_distinct_association_includes_and_limit
author = Author.first
- assert_equal false, author.unique_categorized_posts.includes(:special_comments).limit(0).exists?
- assert_equal true, author.unique_categorized_posts.includes(:special_comments).limit(1).exists?
+ unique_categorized_posts = author.unique_categorized_posts.includes(:special_comments)
+ assert_no_queries { assert_equal false, unique_categorized_posts.limit(0).exists? }
+ assert_queries(1) { assert_equal true, unique_categorized_posts.limit(1).exists? }
end
def test_exists_with_distinct_association_includes_limit_and_order
author = Author.first
- assert_equal false, author.unique_categorized_posts.includes(:special_comments).order("comments.tags_count DESC").limit(0).exists?
- assert_equal true, author.unique_categorized_posts.includes(:special_comments).order("comments.tags_count DESC").limit(1).exists?
+ unique_categorized_posts = author.unique_categorized_posts.includes(:special_comments).order("comments.tags_count DESC")
+ assert_no_queries { assert_equal false, unique_categorized_posts.limit(0).exists? }
+ assert_queries(1) { assert_equal true, unique_categorized_posts.limit(1).exists? }
+ end
+
+ def test_exists_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association
+ ratings = developers(:david).ratings.includes(comment: :post).where(posts: { id: 1 })
+ assert_queries(1) { assert_not_predicate ratings.limit(1), :exists? }
end
def test_exists_with_empty_table_and_no_args_given
@@ -516,7 +565,7 @@ class FinderTest < ActiveRecord::TestCase
assert_nil Topic.offset(4).second_to_last
assert_nil Topic.offset(5).second_to_last
- #test with limit
+ # test with limit
assert_nil Topic.limit(1).second
assert_nil Topic.limit(1).second_to_last
end
@@ -599,13 +648,13 @@ class FinderTest < ActiveRecord::TestCase
def test_last_with_integer_and_order_should_use_sql_limit
relation = Topic.order("title")
assert_queries(1) { relation.last(5) }
- assert !relation.loaded?
+ assert_not_predicate relation, :loaded?
end
def test_last_with_integer_and_reorder_should_use_sql_limit
relation = Topic.reorder("title")
assert_queries(1) { relation.last(5) }
- assert !relation.loaded?
+ assert_not_predicate relation, :loaded?
end
def test_last_on_loaded_relation_should_not_use_sql
@@ -618,7 +667,7 @@ class FinderTest < ActiveRecord::TestCase
def test_last_with_irreversible_order
assert_raises(ActiveRecord::IrreversibleOrderError) do
- Topic.order("coalesce(author_name, title)").last
+ Topic.order(Arel.sql("coalesce(author_name, title)")).last
end
end
@@ -630,12 +679,34 @@ class FinderTest < ActiveRecord::TestCase
assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2)
assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3)
+ assert_equal comments.offset(2).to_a.last, comments.offset(2).last
+ assert_equal comments.offset(2).to_a.last(2), comments.offset(2).last(2)
+ assert_equal comments.offset(2).to_a.last(3), comments.offset(2).last(3)
+
comments = comments.offset(1)
assert_equal comments.limit(2).to_a.last, comments.limit(2).last
assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2)
assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3)
end
+ def test_first_on_relation_with_limit_and_offset
+ post = posts("sti_comments")
+
+ comments = post.comments.order(id: :asc)
+ assert_equal comments.limit(2).to_a.first, comments.limit(2).first
+ assert_equal comments.limit(2).to_a.first(2), comments.limit(2).first(2)
+ assert_equal comments.limit(2).to_a.first(3), comments.limit(2).first(3)
+
+ assert_equal comments.offset(2).to_a.first, comments.offset(2).first
+ assert_equal comments.offset(2).to_a.first(2), comments.offset(2).first(2)
+ assert_equal comments.offset(2).to_a.first(3), comments.offset(2).first(3)
+
+ comments = comments.offset(1)
+ assert_equal comments.limit(2).to_a.first, comments.limit(2).first
+ assert_equal comments.limit(2).to_a.first(2), comments.limit(2).first(2)
+ assert_equal comments.limit(2).to_a.first(3), comments.limit(2).first(3)
+ 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)
@@ -656,8 +727,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
@@ -741,6 +812,15 @@ class FinderTest < ActiveRecord::TestCase
assert_equal [1, 2, 6, 7, 8], Comment.where(id: [1..2, 6..8]).to_a.map(&:id).sort
end
+ def test_find_on_hash_conditions_with_open_ended_range
+ assert_equal [1, 2, 3], Comment.where(id: Float::INFINITY..3).to_a.map(&:id).sort
+ end
+
+ def test_find_on_hash_conditions_with_numeric_range_for_string
+ topic = Topic.create!(title: "12 Factor App")
+ assert_equal [topic], Topic.where(title: 10..2).to_a
+ end
+
def test_find_on_multiple_hash_conditions
assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1)
assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) }
@@ -797,6 +877,25 @@ class FinderTest < ActiveRecord::TestCase
assert_equal customers(:david), found_customer
end
+ def test_hash_condition_find_with_aggregate_having_three_mappings_array
+ david_address = customers(:david).address
+ zaphod_address = customers(:zaphod).address
+ barney_address = customers(:barney).address
+ assert_kind_of Address, david_address
+ assert_kind_of Address, zaphod_address
+ found_customers = Customer.where(address: [david_address, zaphod_address, barney_address])
+ assert_equal [customers(:david), customers(:zaphod), customers(:barney)], found_customers.sort_by(&:id)
+ end
+
+ def test_hash_condition_find_with_aggregate_having_one_mapping_array
+ david_balance = customers(:david).balance
+ zaphod_balance = customers(:zaphod).balance
+ assert_kind_of Money, david_balance
+ 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)
+ end
+
def test_hash_condition_find_with_aggregate_attribute_having_same_name_as_field_and_key_value_being_aggregate
gps_location = customers(:david).gps_location
assert_kind_of GpsLocation, gps_location
@@ -1002,14 +1101,6 @@ class FinderTest < ActiveRecord::TestCase
assert_raise(ArgumentError) { Topic.find_by_title_and_author_name("The First Topic") }
end
- def test_find_last_with_offset
- devs = Developer.order("id")
-
- assert_equal devs[2], Developer.offset(2).first
- assert_equal devs[-3], Developer.offset(2).last
- assert_equal devs[-3], Developer.offset(2).order("id DESC").first
- end
-
def test_find_by_nil_attribute
topic = Topic.find_by_last_read nil
assert_not_nil topic
@@ -1098,6 +1189,11 @@ class FinderTest < ActiveRecord::TestCase
order("author_addresses_authors.id DESC").limit(3).to_a.size
end
+ def test_find_with_eager_loading_collection_and_ordering_by_collection_primary_key
+ assert_equal Post.first, Post.eager_load(comments: :ratings).
+ order("posts.id, ratings.id, comments.id").first
+ end
+
def test_find_with_nil_inside_set_passed_for_one_attribute
client_of = Company.
where(client_of: [2, 1, nil],
@@ -1189,7 +1285,7 @@ class FinderTest < ActiveRecord::TestCase
test "find_by with associations" do
assert_equal authors(:david), Post.find_by(author: authors(:david)).author
- assert_equal authors(:mary) , Post.find_by(author: authors(:mary)).author
+ assert_equal authors(:mary), Post.find_by(author: authors(:mary)).author
end
test "find_by doesn't have implicit ordering" do
@@ -1254,12 +1350,12 @@ class FinderTest < ActiveRecord::TestCase
test "#skip_query_cache! for #exists? with a limited eager load" do
Topic.cache do
- assert_queries(2) do
+ assert_queries(1) do
Topic.eager_load(:replies).limit(1).exists?
Topic.eager_load(:replies).limit(1).exists?
end
- assert_queries(4) do
+ assert_queries(2) do
Topic.eager_load(:replies).limit(1).skip_query_cache!.exists?
Topic.eager_load(:replies).limit(1).skip_query_cache!.exists?
end
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index b0b63f5203..a4fa3c285b 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -79,6 +79,151 @@ class FixturesTest < ActiveRecord::TestCase
ActiveSupport::Notifications.unsubscribe(subscription)
end
end
+
+ def test_bulk_insert_multiple_table_with_a_multi_statement_query
+ subscriber = InsertQuerySubscriber.new
+ subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber)
+
+ create_fixtures("bulbs", "authors", "computers")
+
+ expected_sql = <<~EOS.chop
+ INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("bulbs")} .*
+ INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("authors")} .*
+ INSERT INTO #{ActiveRecord::Base.connection.quote_table_name("computers")} .*
+ EOS
+ assert_equal 1, subscriber.events.size
+ assert_match(/#{expected_sql}/, subscriber.events.first)
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscription)
+ end
+
+ def test_bulk_insert_with_a_multi_statement_query_raises_an_exception_when_any_insert_fails
+ require "models/aircraft"
+
+ assert_equal false, Aircraft.columns_hash["wheels_count"].null
+ fixtures = {
+ "aircraft" => [
+ { "name" => "working_aircrafts", "wheels_count" => 2 },
+ { "name" => "broken_aircrafts", "wheels_count" => nil },
+ ]
+ }
+
+ assert_no_difference "Aircraft.count" do
+ assert_raises(ActiveRecord::NotNullViolation) do
+ ActiveRecord::Base.connection.insert_fixtures_set(fixtures)
+ end
+ end
+ end
+ end
+
+ if current_adapter?(:Mysql2Adapter)
+ 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
+ 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)
+ end
+
+ def test_insert_fixture_set_when_max_allowed_packet_is_bigger_than_fixtures_set_size
+ conn = ActiveRecord::Base.connection
+ packet_size = 1024
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 51] },
+ ]
+ }
+
+ conn.stubs(:max_allowed_packet).returns(packet_size)
+
+ assert_difference "TrafficLight.count" do
+ conn.insert_fixtures_set(fixtures)
+ end
+ end
+
+ def test_insert_fixtures_set_split_the_total_sql_into_two_chunks_smaller_than_max_allowed_packet
+ subscriber = InsertQuerySubscriber.new
+ subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber)
+ conn = ActiveRecord::Base.connection
+ packet_size = 1024
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 450] },
+ ],
+ "comments" => [
+ { "post_id" => 1, "body" => "a" * 450 },
+ ]
+ }
+
+ conn.stubs(:max_allowed_packet).returns(packet_size)
+
+ 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
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscription)
+ end
+
+ def test_insert_fixtures_set_concat_total_sql_into_a_single_packet_smaller_than_max_allowed_packet
+ subscriber = InsertQuerySubscriber.new
+ subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber)
+ conn = ActiveRecord::Base.connection
+ packet_size = 1024
+ fixtures = {
+ "traffic_lights" => [
+ { "location" => "US", "state" => ["NY"], "long_state" => ["a" * 200] },
+ ],
+ "comments" => [
+ { "post_id" => 1, "body" => "a" * 200 },
+ ]
+ }
+
+ conn.stubs(:max_allowed_packet).returns(packet_size)
+
+ assert_difference ["TrafficLight.count", "Comment.count"], +1 do
+ conn.insert_fixtures_set(fixtures)
+ end
+ assert_equal 1, subscriber.events.size
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscription)
+ end
+ end
+
+ def test_auto_value_on_primary_key
+ fixtures = [
+ { "name" => "first", "wheels_count" => 2 },
+ { "name" => "second", "wheels_count" => 3 }
+ ]
+ conn = ActiveRecord::Base.connection
+ assert_nothing_raised do
+ conn.insert_fixtures_set({ "aircraft" => 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_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
@@ -247,8 +392,8 @@ class FixturesTest < ActiveRecord::TestCase
def test_nonexistent_fixture_file
nonexistent_fixture_path = FIXTURES_ROOT + "/imnothere"
- #sanity check to make sure that this file never exists
- assert Dir[nonexistent_fixture_path + "*"].empty?
+ # sanity check to make sure that this file never exists
+ assert_empty Dir[nonexistent_fixture_path + "*"]
assert_raise(Errno::ENOENT) do
ActiveRecord::FixtureSet.new(Account.connection, "companies", Company, nonexistent_fixture_path)
@@ -447,10 +592,10 @@ 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
@@ -937,13 +1082,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
@@ -998,10 +1143,10 @@ class FoxyFixturesTest < ActiveRecord::TestCase
end
def test_resolves_enums
- assert books(:awdr).published?
- assert books(:awdr).read?
- assert books(:rfr).proposed?
- assert books(:ddd).published?
+ assert_predicate books(:awdr), :published?
+ assert_predicate books(:awdr), :read?
+ assert_predicate books(:rfr), :proposed?
+ assert_predicate books(:ddd), :published?
end
end
@@ -1041,7 +1186,7 @@ class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase
end
def test_table_name_is_defined_in_the_model
- assert_equal "randomly_named_table2", ActiveRecord::FixtureSet::all_loaded_fixtures["admin/randomly_named_a9"].table_name
+ assert_equal "randomly_named_table2", ActiveRecord::FixtureSet.all_loaded_fixtures["admin/randomly_named_a9"].table_name
assert_equal "randomly_named_table2", Admin::ClassNameThatDoesNotFollowCONVENTIONS1.table_name
end
end
diff --git a/activerecord/test/cases/habtm_destroy_order_test.rb b/activerecord/test/cases/habtm_destroy_order_test.rb
index 5e503272e1..b15e1b48c4 100644
--- a/activerecord/test/cases/habtm_destroy_order_test.rb
+++ b/activerecord/test/cases/habtm_destroy_order_test.rb
@@ -15,7 +15,7 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase
sicp.destroy
end
end
- assert !sicp.destroyed?
+ assert_not_predicate sicp, :destroyed?
end
test "should not raise error if have foreign key in the join table" do
@@ -42,7 +42,7 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase
ben.lessons << sicp
ben.save!
ben.destroy
- assert !ben.reload.lessons.empty?
+ assert_not_empty ben.reload.lessons
ensure
# get rid of it so Student is still like it was
Student.reset_callbacks(:destroy)
@@ -58,6 +58,6 @@ class HabtmDestroyOrderTest < ActiveRecord::TestCase
assert_raises LessonError do
sicp.destroy
end
- assert !sicp.reload.students.empty?
+ assert_not_empty sicp.reload.students
end
end
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index 6ea02ac191..66f11fe5bd 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -184,4 +184,4 @@ module InTimeZone
end
end
-require "mocha/setup" # FIXME: stop using mocha
+require "mocha/minitest" # FIXME: stop using mocha
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index c931f7d21c..7a5c06b894 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -130,61 +130,70 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_descends_from_active_record
- assert !ActiveRecord::Base.descends_from_active_record?
+ assert_not_predicate ActiveRecord::Base, :descends_from_active_record?
# Abstract subclass of AR::Base.
- assert LoosePerson.descends_from_active_record?
+ assert_predicate LoosePerson, :descends_from_active_record?
# Concrete subclass of an abstract class.
- assert LooseDescendant.descends_from_active_record?
+ assert_predicate LooseDescendant, :descends_from_active_record?
# Concrete subclass of AR::Base.
- assert TightPerson.descends_from_active_record?
+ assert_predicate TightPerson, :descends_from_active_record?
# Concrete subclass of a concrete class but has no type column.
- assert TightDescendant.descends_from_active_record?
+ assert_predicate TightDescendant, :descends_from_active_record?
# Concrete subclass of AR::Base.
- assert Post.descends_from_active_record?
+ assert_predicate Post, :descends_from_active_record?
# Concrete subclasses of a concrete class which has a type column.
- assert !StiPost.descends_from_active_record?
- assert !SubStiPost.descends_from_active_record?
+ assert_not_predicate StiPost, :descends_from_active_record?
+ assert_not_predicate SubStiPost, :descends_from_active_record?
# Abstract subclass of a concrete class which has a type column.
# This is pathological, as you'll never have Sub < Abstract < Concrete.
- assert !AbstractStiPost.descends_from_active_record?
+ assert_not_predicate AbstractStiPost, :descends_from_active_record?
# Concrete subclass of an abstract class which has a type column.
- assert !SubAbstractStiPost.descends_from_active_record?
+ assert_not_predicate SubAbstractStiPost, :descends_from_active_record?
end
def test_company_descends_from_active_record
- assert !ActiveRecord::Base.descends_from_active_record?
+ 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"
end
def test_abstract_class
- assert !ActiveRecord::Base.abstract_class?
- assert LoosePerson.abstract_class?
- assert !LooseDescendant.abstract_class?
+ assert_not_predicate ActiveRecord::Base, :abstract_class?
+ assert_predicate LoosePerson, :abstract_class?
+ assert_not_predicate LooseDescendant, :abstract_class?
end
def test_inheritance_base_class
assert_equal Post, Post.base_class
+ assert_predicate Post, :base_class?
assert_equal Post, SpecialPost.base_class
+ assert_not_predicate SpecialPost, :base_class?
assert_equal Post, StiPost.base_class
+ assert_not_predicate StiPost, :base_class?
assert_equal Post, SubStiPost.base_class
+ assert_not_predicate SubStiPost, :base_class?
assert_equal SubAbstractStiPost, SubAbstractStiPost.base_class
+ assert_predicate SubAbstractStiPost, :base_class?
end
def test_abstract_inheritance_base_class
assert_equal LoosePerson, LoosePerson.base_class
+ assert_predicate LoosePerson, :base_class?
assert_equal LooseDescendant, LooseDescendant.base_class
+ assert_predicate LooseDescendant, :base_class?
assert_equal TightPerson, TightPerson.base_class
+ assert_predicate TightPerson, :base_class?
assert_equal TightPerson, TightDescendant.base_class
+ assert_not_predicate TightDescendant, :base_class?
end
def test_base_class_activerecord_error
@@ -280,6 +289,21 @@ class InheritanceTest < ActiveRecord::TestCase
assert_equal Firm, firm.class
end
+ def test_where_new_with_subclass
+ firm = Company.where(type: "Firm").new
+ assert_equal Firm, firm.class
+ end
+
+ def test_where_create_with_subclass
+ firm = Company.where(type: "Firm").create(name: "Basecamp")
+ assert_equal Firm, firm.class
+ end
+
+ def test_where_create_bang_with_subclass
+ firm = Company.where(type: "Firm").create!(name: "Basecamp")
+ assert_equal Firm, firm.class
+ end
+
def test_new_with_abstract_class
e = assert_raises(NotImplementedError) do
AbstractCompany.new
@@ -302,6 +326,30 @@ class InheritanceTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::SubclassNotFound) { Company.new(type: "Account") }
end
+ def test_where_new_with_invalid_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "InvalidType").new }
+ end
+
+ def test_where_new_with_unrelated_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "Account").new }
+ end
+
+ def test_where_create_with_invalid_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "InvalidType").create }
+ end
+
+ def test_where_create_with_unrelated_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "Account").create }
+ end
+
+ def test_where_create_bang_with_invalid_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "InvalidType").create! }
+ end
+
+ def test_where_create_bang_with_unrelated_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.where(type: "Account").create! }
+ end
+
def test_new_with_unrelated_namespaced_type
without_store_full_sti_class do
e = assert_raises ActiveRecord::SubclassNotFound do
diff --git a/activerecord/test/cases/instrumentation_test.rb b/activerecord/test/cases/instrumentation_test.rb
new file mode 100644
index 0000000000..e6e8468757
--- /dev/null
+++ b/activerecord/test/cases/instrumentation_test.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/book"
+
+module ActiveRecord
+ class InstrumentationTest < ActiveRecord::TestCase
+ def test_payload_name_on_load
+ Book.create(name: "test book")
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ if event.payload[:sql].match "SELECT"
+ assert_equal "Book Load", event.payload[:name]
+ end
+ end
+ Book.first
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ def test_payload_name_on_create
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ if event.payload[:sql].match "INSERT"
+ assert_equal "Book Create", event.payload[:name]
+ end
+ end
+ Book.create(name: "test book")
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ def test_payload_name_on_update
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ if event.payload[:sql].match "UPDATE"
+ assert_equal "Book Update", event.payload[:name]
+ end
+ end
+ book = Book.create(name: "test book")
+ book.update_attribute(:name, "new name")
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ def test_payload_name_on_update_all
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ if event.payload[:sql].match "UPDATE"
+ assert_equal "Book Update All", event.payload[:name]
+ end
+ end
+ Book.create(name: "test book")
+ Book.update_all(name: "new name")
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+
+ def test_payload_name_on_destroy
+ subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |*args|
+ event = ActiveSupport::Notifications::Event.new(*args)
+ if event.payload[:sql].match "DELETE"
+ assert_equal "Book Destroy", event.payload[:name]
+ end
+ end
+ book = Book.create(name: "test book")
+ book.destroy
+ ensure
+ ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
+ end
+ end
+end
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index 20e747142b..9c85543b9b 100644
--- a/activerecord/test/cases/invertible_migration_test.rb
+++ b/activerecord/test/cases/invertible_migration_test.rb
@@ -161,6 +161,15 @@ module ActiveRecord
end
end
+ class UpOnlyMigration < SilentMigration
+ def change
+ add_column :horses, :oldie, :integer, default: 0
+ up_only { execute "update horses set oldie = 1" }
+ end
+ end
+
+ self.use_transactional_tests = false
+
setup do
@verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, false
end
@@ -206,7 +215,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
@@ -214,11 +223,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
@@ -232,12 +241,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
@@ -246,11 +255,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
@@ -259,7 +268,7 @@ 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_change_column_default
@@ -295,6 +304,8 @@ module ActiveRecord
migration2.migrate(:down)
assert_equal false, Horse.connection.extension_enabled?("hstore")
+ ensure
+ enable_extension!("hstore", ActiveRecord::Base.connection)
end
end
@@ -376,5 +387,23 @@ module ActiveRecord
"horses_index_named index should not exist"
end
end
+
+ def test_up_only
+ InvertibleMigration.new.migrate(:up)
+ horse1 = Horse.create
+ # populates existing horses with oldie = 1 but new ones have default 0
+ UpOnlyMigration.new.migrate(:up)
+ Horse.reset_column_information
+ horse1.reload
+ horse2 = Horse.create
+
+ assert 1, horse1.oldie # created before migration
+ assert 0, horse2.oldie # created after migration
+
+ UpOnlyMigration.new.migrate(:down) # should be no error
+ connection = ActiveRecord::Base.connection
+ assert_not connection.column_exists?(:horses, :oldie)
+ Horse.reset_column_information
+ end
end
end
diff --git a/activerecord/test/cases/json_attribute_test.rb b/activerecord/test/cases/json_attribute_test.rb
index 63f3c77fc3..afc39d0420 100644
--- a/activerecord/test/cases/json_attribute_test.rb
+++ b/activerecord/test/cases/json_attribute_test.rb
@@ -19,14 +19,14 @@ class JsonAttributeTest < ActiveRecord::TestCase
def setup
super
@connection.create_table("json_data_type") do |t|
- t.text "payload"
- t.text "settings"
+ t.string "payload"
+ t.string "settings"
end
end
private
def column_type
- :text
+ :string
end
def klass
diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb
index 52fe488cd5..82cf281cff 100644
--- a/activerecord/test/cases/json_serialization_test.rb
+++ b/activerecord/test/cases/json_serialization_test.rb
@@ -252,7 +252,7 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
def @david.favorite_quote; "Constraints are liberating"; end
json = @david.to_json(include: :posts, methods: :favorite_quote)
- assert !@david.posts.first.respond_to?(:favorite_quote)
+ assert_not_respond_to @david.posts.first, :favorite_quote
assert_match %r{"favorite_quote":"Constraints are liberating"}, json
assert_equal 1, %r{"favorite_quote":}.match(json).size
end
diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb
index 56ec8c8a82..9b79803503 100644
--- a/activerecord/test/cases/json_shared_test_cases.rb
+++ b/activerecord/test/cases/json_shared_test_cases.rb
@@ -23,10 +23,10 @@ module JSONSharedTestCases
def test_column
column = klass.columns_hash["payload"]
assert_equal column_type, column.type
- assert_equal column_type.to_s, column.sql_type
+ assert_type_match column_type, column.sql_type
type = klass.type_for_attribute("payload")
- assert_not type.binary?
+ assert_not_predicate type, :binary?
end
def test_change_table_supports_json
@@ -36,7 +36,7 @@ module JSONSharedTestCases
klass.reset_column_information
column = klass.columns_hash["users"]
assert_equal column_type, column.type
- assert_equal column_type.to_s, column.sql_type
+ assert_type_match column_type, column.sql_type
end
def test_schema_dumping
@@ -66,26 +66,26 @@ module JSONSharedTestCases
end
def test_rewrite
- @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|)
+ @connection.execute(insert_statement_per_database('{"k":"v"}'))
x = klass.first
x.payload = { '"a\'' => "b" }
assert x.save!
end
def test_select
- @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|)
+ @connection.execute(insert_statement_per_database('{"k":"v"}'))
x = klass.first
assert_equal({ "k" => "v" }, x.payload)
end
def test_select_multikey
- @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')|)
+ @connection.execute(insert_statement_per_database('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}'))
x = klass.first
assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1, 2, 3] }, x.payload)
end
def test_null_json
- @connection.execute("insert into json_data_type (payload) VALUES(null)")
+ @connection.execute(insert_statement_per_database("null"))
x = klass.first
assert_nil(x.payload)
end
@@ -101,19 +101,19 @@ module JSONSharedTestCases
x = klass.where(payload: nil).first
assert_nil(x)
- json.update_attributes(payload: nil)
+ json.update(payload: nil)
x = klass.where(payload: nil).first
assert_equal(json.reload, x)
end
def test_select_array_json_value
- @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|)
+ @connection.execute(insert_statement_per_database('["v0",{"k1":"v1"}]'))
x = klass.first
assert_equal(["v0", { "k1" => "v1" }], x.payload)
end
def test_rewrite_array_json_value
- @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|)
+ @connection.execute(insert_statement_per_database('["v0",{"k1":"v1"}]'))
x = klass.first
x.payload = ["v1", { "k2" => "v2" }, "v3"]
assert x.save!
@@ -152,42 +152,42 @@ module JSONSharedTestCases
def test_changes_in_place
json = klass.new
- assert_not json.changed?
+ assert_not_predicate json, :changed?
json.payload = { "one" => "two" }
- assert json.changed?
- assert json.payload_changed?
+ assert_predicate json, :changed?
+ assert_predicate json, :payload_changed?
json.save!
- assert_not json.changed?
+ assert_not_predicate json, :changed?
json.payload["three"] = "four"
- assert json.payload_changed?
+ assert_predicate json, :payload_changed?
json.save!
json.reload
assert_equal({ "one" => "two", "three" => "four" }, json.payload)
- assert_not json.changed?
+ assert_not_predicate json, :changed?
end
def test_changes_in_place_ignores_key_order
json = klass.new
- assert_not json.changed?
+ assert_not_predicate json, :changed?
json.payload = { "three" => "four", "one" => "two" }
json.save!
json.reload
json.payload = { "three" => "four", "one" => "two" }
- assert_not json.changed?
+ assert_not_predicate json, :changed?
json.payload = [{ "three" => "four", "one" => "two" }, { "seven" => "eight", "five" => "six" }]
json.save!
json.reload
json.payload = [{ "three" => "four", "one" => "two" }, { "seven" => "eight", "five" => "six" }]
- assert_not json.changed?
+ assert_not_predicate json, :changed?
end
def test_changes_in_place_with_ruby_object
@@ -195,10 +195,10 @@ module JSONSharedTestCases
json = klass.create!(payload: time)
json.reload
- assert_not json.changed?
+ assert_not_predicate json, :changed?
json.payload = time
- assert_not json.changed?
+ assert_not_predicate json, :changed?
end
def test_assigning_string_literal
@@ -253,4 +253,17 @@ module JSONSharedTestCases
def klass
JsonDataType
end
+
+ def assert_type_match(type, sql_type)
+ native_type = ActiveRecord::Base.connection.native_database_types[type][:name]
+ assert_match %r(\A#{native_type}\b), sql_type
+ end
+
+ def insert_statement_per_database(values)
+ if current_adapter?(:OracleAdapter)
+ "insert into json_data_type (id, payload) VALUES (json_data_type_seq.nextval, '#{values}')"
+ else
+ "insert into json_data_type (payload) VALUES ('#{values}')"
+ end
+ end
end
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 743680ba92..8513edb0ab 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -15,6 +15,7 @@ require "models/bulb"
require "models/engine"
require "models/wheel"
require "models/treasure"
+require "models/frog"
class LockWithoutDefault < ActiveRecord::Base; end
@@ -69,8 +70,8 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::StaleObjectError) { s2.destroy }
assert s1.destroy
- assert s1.frozen?
- assert s1.destroyed?
+ assert_predicate s1, :frozen?
+ assert_predicate s1, :destroyed?
assert_raises(ActiveRecord::RecordNotFound) { StringKeyObject.find("record1") }
end
@@ -104,8 +105,8 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::StaleObjectError) { p2.destroy }
assert p1.destroy
- assert p1.frozen?
- assert p1.destroyed?
+ assert_predicate p1, :frozen?
+ assert_predicate p1, :destroyed?
assert_raises(ActiveRecord::RecordNotFound) { Person.find(1) }
end
@@ -194,6 +195,45 @@ class OptimisticLockingTest < ActiveRecord::TestCase
end
end
+ def test_update_with_dirty_primary_key
+ assert_raises(ActiveRecord::RecordNotUnique) do
+ person = Person.find(1)
+ person.id = 2
+ person.save!
+ end
+
+ person = Person.find(1)
+ person.id = 42
+ person.save!
+
+ assert Person.find(42)
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Person.find(1)
+ end
+ end
+
+ def test_delete_with_dirty_primary_key
+ person = Person.find(1)
+ person.id = 2
+ person.delete
+
+ assert Person.find(2)
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Person.find(1)
+ end
+ end
+
+ def test_destroy_with_dirty_primary_key
+ person = Person.find(1)
+ person.id = 2
+ person.destroy
+
+ assert Person.find(2)
+ assert_raises(ActiveRecord::RecordNotFound) do
+ Person.find(1)
+ end
+ end
+
def test_explicit_update_lock_column_raise_error
person = Person.find(1)
@@ -201,7 +241,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase
person.first_name = "Douglas Adams"
person.lock_version = 42
- assert person.lock_version_changed?
+ assert_predicate person, :lock_version_changed?
person.save
end
@@ -399,25 +439,59 @@ class OptimisticLockingTest < ActiveRecord::TestCase
end
end
+ def test_counter_cache_with_touch_and_lock_version
+ car = Car.create!
+
+ assert_equal 0, car.wheels_count
+ assert_equal 0, car.lock_version
+
+ previously_car_updated_at = car.updated_at
+ travel(2.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
+
+ previously_car_updated_at = car.updated_at
+ travel(1.day) 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
+
+ previously_car_updated_at = car.updated_at
+ travel(2.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
+ end
+
def test_polymorphic_destroy_with_dependencies_and_lock_version
car = Car.create!
assert_difference "car.wheels.count" do
- car.wheels << Wheel.create!
+ car.wheels.create
end
assert_difference "car.wheels.count", -1 do
car.reload.destroy
end
- assert car.destroyed?
+ assert_predicate car, :destroyed?
end
def test_removing_has_and_belongs_to_many_associations_upon_destroy
p = RichPerson.create! first_name: "Jon"
p.treasures.create!
- assert !p.treasures.empty?
+ assert_not_empty p.treasures
p.destroy
- assert p.treasures.empty?
- assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty?
+ assert_empty p.treasures
+ assert_empty RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1")
end
def test_yaml_dumping_with_lock_column
@@ -487,7 +561,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
t1.destroy
- assert t1.destroyed?
+ assert_predicate t1, :destroyed?
end
def test_destroy_stale_object
@@ -500,7 +574,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
stale_object.destroy!
end
- refute stale_object.destroyed?
+ assert_not_predicate stale_object, :destroyed?
end
private
@@ -565,17 +639,27 @@ unless in_memory_db?
end
end
- # Locking a record reloads it.
- def test_sane_lock_method
+ def test_lock_does_not_raise_when_the_object_is_not_dirty
+ person = Person.find 1
assert_nothing_raised do
- Person.transaction do
- person = Person.find 1
- old, person.first_name = person.first_name, "fooman"
- # Locking a dirty record is deprecated
- assert_deprecated do
- person.lock!
- end
- assert_equal old, person.first_name
+ person.lock!
+ end
+ end
+
+ def test_lock_raises_when_the_record_is_dirty
+ person = Person.find 1
+ person.first_name = "fooman"
+ assert_raises(RuntimeError) do
+ person.lock!
+ end
+ end
+
+ def test_locking_in_after_save_callback
+ assert_nothing_raised do
+ frog = ::Frog.create(name: "Old Frog")
+ frog.name = "New Frog"
+ assert_not_deprecated do
+ frog.save!
end
end
end
@@ -611,14 +695,12 @@ unless in_memory_db?
end
end
- if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
- def test_no_locks_no_wait
- first, second = duel { Person.find 1 }
- assert first.end > second.end
- end
-
- private
+ def test_no_locks_no_wait
+ first, second = duel { Person.find 1 }
+ assert first.end > second.end
+ end
+ private
def duel(zzz = 5)
t0, t1, t2, t3 = nil, nil, nil, nil
@@ -646,6 +728,5 @@ unless in_memory_db?
assert t3 > t2
[t0.to_f..t1.to_f, t2.to_f..t3.to_f]
end
- end
end
end
diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb
index 208e54ed0b..e2742ed33e 100644
--- a/activerecord/test/cases/log_subscriber_test.rb
+++ b/activerecord/test/cases/log_subscriber_test.rb
@@ -33,8 +33,9 @@ class LogSubscriberTest < ActiveRecord::TestCase
super
end
- def debug(message)
- @debugs << message
+ def debug(progname = nil, &block)
+ @debugs << progname
+ super
end
end
@@ -171,6 +172,22 @@ class LogSubscriberTest < ActiveRecord::TestCase
assert_match(/SELECT .*?FROM .?developers.?/i, @logger.logged(:debug).last)
end
+ def test_vebose_query_logs
+ ActiveRecord::Base.verbose_query_logs = true
+
+ logger = TestDebugLogSubscriber.new
+ logger.sql(Event.new(0, sql: "hi mom!"))
+ assert_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!"))
+ assert_no_match(/↳/, @logger.logged(:debug).last)
+ end
+
def test_cached_queries
ActiveRecord::Base.cache do
Developer.all.load
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index 7b0644e9c0..f4d16cb093 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -84,7 +84,7 @@ module ActiveRecord
columns = connection.columns(:testings)
array_column = columns.detect { |c| c.name == "foo" }
- assert array_column.array?
+ assert_predicate array_column, :array?
end
def test_create_table_with_array_column
@@ -95,7 +95,7 @@ module ActiveRecord
columns = connection.columns(:testings)
array_column = columns.detect { |c| c.name == "foo" }
- assert array_column.array?
+ assert_predicate array_column, :array?
end
end
@@ -205,8 +205,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
@@ -264,19 +264,18 @@ module ActiveRecord
t.column :foo, :timestamp
end
- klass = Class.new(ActiveRecord::Base)
- klass.table_name = "testings"
+ column = connection.columns(:testings).find { |c| c.name == "foo" }
- assert_equal :datetime, klass.columns_hash["foo"].type
+ assert_equal :datetime, column.type
if current_adapter?(:PostgreSQLAdapter)
- assert_equal "timestamp without time zone", klass.columns_hash["foo"].sql_type
+ assert_equal "timestamp without time zone", column.sql_type
elsif current_adapter?(:Mysql2Adapter)
- assert_equal "timestamp", klass.columns_hash["foo"].sql_type
+ assert_equal "timestamp", column.sql_type
elsif current_adapter?(:OracleAdapter)
- assert_equal "TIMESTAMP(6)", klass.columns_hash["foo"].sql_type
+ assert_equal "TIMESTAMP(6)", column.sql_type
else
- assert_equal klass.connection.type_to_sql("datetime"), klass.columns_hash["foo"].sql_type
+ assert_equal connection.type_to_sql("datetime"), column.sql_type
end
end
@@ -409,7 +408,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
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
index be6dc2acb1..3022121f4c 100644
--- a/activerecord/test/cases/migration/column_attributes_test.rb
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -80,7 +80,7 @@ module ActiveRecord
TestModel.delete_all
# Now use the Rails insertion
- TestModel.create wealth: BigDecimal.new("12345678901234567890.0123456789")
+ TestModel.create wealth: BigDecimal("12345678901234567890.0123456789")
# SELECT
row = TestModel.first
@@ -146,7 +146,7 @@ module ActiveRecord
TestModel.create first_name: "bob", last_name: "bobsen",
bio: "I was born ....", age: 18, height: 1.78,
- wealth: BigDecimal.new("12345678901234567890.0123456789"),
+ wealth: BigDecimal("12345678901234567890.0123456789"),
birthday: 18.years.ago, favorite_day: 10.days.ago,
moment_of_truth: "1782-10-10 21:40:18", male: true
@@ -159,7 +159,7 @@ module ActiveRecord
# Test for 30 significant digits (beyond the 16 of float), 10 of them
# after the decimal place.
- assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth
+ assert_equal BigDecimal("0012345678901234567890.0123456789"), bob.wealth
assert_equal true, bob.male?
diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb
index 8ca20b6172..cedd9c44e3 100644
--- a/activerecord/test/cases/migration/columns_test.rb
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -67,7 +67,7 @@ module ActiveRecord
if current_adapter?(:Mysql2Adapter)
def test_mysql_rename_column_preserves_auto_increment
rename_column "test_models", "id", "id_test"
- assert connection.columns("test_models").find { |c| c.name == "id_test" }.auto_increment?
+ assert_predicate connection.columns("test_models").find { |c| c.name == "id_test" }, :auto_increment?
TestModel.reset_column_information
ensure
rename_column "test_models", "id_test", "id"
@@ -223,31 +223,31 @@ module ActiveRecord
def test_change_column_with_nil_default
add_column "test_models", "contributor", :boolean, default: true
- assert TestModel.new.contributor?
+ assert_predicate TestModel.new, :contributor?
change_column "test_models", "contributor", :boolean, default: nil
TestModel.reset_column_information
- assert_not TestModel.new.contributor?
+ assert_not_predicate TestModel.new, :contributor?
assert_nil TestModel.new.contributor
end
def test_change_column_to_drop_default_with_null_false
add_column "test_models", "contributor", :boolean, default: true, null: false
- assert TestModel.new.contributor?
+ assert_predicate TestModel.new, :contributor?
change_column "test_models", "contributor", :boolean, default: nil, null: false
TestModel.reset_column_information
- assert_not TestModel.new.contributor?
+ assert_not_predicate TestModel.new, :contributor?
assert_nil TestModel.new.contributor
end
def test_change_column_with_new_default
add_column "test_models", "administrator", :boolean, default: true
- assert TestModel.new.administrator?
+ assert_predicate TestModel.new, :administrator?
change_column "test_models", "administrator", :boolean, default: false
TestModel.reset_column_information
- assert_not TestModel.new.administrator?
+ assert_not_predicate TestModel.new, :administrator?
end
def test_change_column_with_custom_index_name
diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb
index 0b5e983f14..3a11bb081b 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -14,7 +14,7 @@ module ActiveRecord
recorder = CommandRecorder.new(Class.new {
def america; end
}.new)
- assert recorder.respond_to?(:america)
+ assert_respond_to recorder, :america
end
def test_send_calls_super
@@ -27,7 +27,7 @@ module ActiveRecord
recorder = CommandRecorder.new(Class.new {
def create_table(name); end
}.new)
- assert recorder.respond_to?(:create_table), "respond_to? create_table"
+ assert_respond_to recorder, :create_table
recorder.send(:create_table, :horses)
assert_equal [[:create_table, [:horses], nil]], recorder.commands
end
@@ -213,6 +213,11 @@ module ActiveRecord
assert_equal [:remove_index, [:table, { name: "new_index" }]], remove
end
+ def test_invert_add_index_with_algorithm_option
+ remove = @recorder.inverse_of :add_index, [:table, :one, algorithm: :concurrently]
+ assert_equal [:remove_index, [:table, { column: :one, algorithm: :concurrently }]], remove
+ end
+
def test_invert_remove_index
add = @recorder.inverse_of :remove_index, [:table, :one]
assert_equal [:add_index, [:table, :one]], add
diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb
index cb3b02c02a..69a50674af 100644
--- a/activerecord/test/cases/migration/compatibility_test.rb
+++ b/activerecord/test/cases/migration/compatibility_test.rb
@@ -16,7 +16,7 @@ module ActiveRecord
ActiveRecord::Migration.verbose = false
connection.create_table :testings do |t|
- t.column :foo, :string, limit: 100
+ t.column :foo, :string, limit: 5
t.column :bar, :string, limit: 100
end
end
@@ -126,15 +126,32 @@ module ActiveRecord
end
assert_match(/LegacyMigration < ActiveRecord::Migration\[4\.2\]/, e.message)
end
+
+ if current_adapter?(:PostgreSQLAdapter)
+ class Testing < ActiveRecord::Base
+ end
+
+ def test_legacy_change_column_with_null_executes_update
+ migration = Class.new(ActiveRecord::Migration[5.1]) {
+ def migrate(x)
+ change_column :testings, :foo, :string, limit: 10, null: false, default: "foobar"
+ end
+ }.new
+
+ Testing.create!
+ ActiveRecord::Migrator.new(:up, [migration]).migrate
+ assert_equal ["foobar"], Testing.all.map(&:foo)
+ ensure
+ ActiveRecord::Base.clear_cache!
+ end
+ end
end
end
end
-class LegacyPrimaryKeyTest < ActiveRecord::TestCase
+module LegacyPrimaryKeyTestCases
include SchemaDumpingHelper
- self.use_transactional_tests = false
-
class LegacyPrimaryKey < ActiveRecord::Base
end
@@ -152,7 +169,7 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase
end
def test_legacy_primary_key_should_be_auto_incremented
- @migration = Class.new(ActiveRecord::Migration[5.0]) {
+ @migration = Class.new(migration_class) {
def change
create_table :legacy_primary_keys do |t|
t.references :legacy_ref
@@ -162,12 +179,10 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase
@migration.migrate(:up)
- legacy_pk = LegacyPrimaryKey.columns_hash["id"]
- assert_not legacy_pk.bigint?
- assert_not legacy_pk.null
+ assert_legacy_primary_key
legacy_ref = LegacyPrimaryKey.columns_hash["legacy_ref_id"]
- assert_not legacy_ref.bigint?
+ assert_not_predicate legacy_ref, :bigint?
record1 = LegacyPrimaryKey.create!
assert_not_nil record1.id
@@ -182,7 +197,7 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase
def test_legacy_integer_primary_key_should_not_be_auto_incremented
skip if current_adapter?(:SQLite3Adapter)
- @migration = Class.new(ActiveRecord::Migration[5.0]) {
+ @migration = Class.new(migration_class) {
def change
create_table :legacy_primary_keys, id: :integer do |t|
end
@@ -199,9 +214,85 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase
assert_match %r{create_table "legacy_primary_keys", id: :integer, default: nil}, schema
end
+ def test_legacy_primary_key_in_create_table_should_be_integer
+ @migration = Class.new(migration_class) {
+ def change
+ create_table :legacy_primary_keys, id: false do |t|
+ t.primary_key :id
+ end
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ assert_legacy_primary_key
+ end
+
+ def test_legacy_primary_key_in_change_table_should_be_integer
+ @migration = Class.new(migration_class) {
+ def change
+ create_table :legacy_primary_keys, id: false do |t|
+ t.integer :dummy
+ end
+ change_table :legacy_primary_keys do |t|
+ t.primary_key :id
+ end
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ assert_legacy_primary_key
+ end
+
+ def test_add_column_with_legacy_primary_key_should_be_integer
+ @migration = Class.new(migration_class) {
+ def change
+ create_table :legacy_primary_keys, id: false do |t|
+ t.integer :dummy
+ end
+ add_column :legacy_primary_keys, :id, :primary_key
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ assert_legacy_primary_key
+ end
+
+ def test_legacy_join_table_foreign_keys_should_be_integer
+ @migration = Class.new(migration_class) {
+ def change
+ create_join_table :apples, :bananas do |t|
+ end
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ schema = dump_table_schema "apples_bananas"
+ assert_match %r{integer "apple_id", null: false}, schema
+ assert_match %r{integer "banana_id", null: false}, schema
+ end
+
+ def test_legacy_join_table_column_options_should_be_overwritten
+ @migration = Class.new(migration_class) {
+ def change
+ create_join_table :apples, :bananas, column_options: { type: :bigint } do |t|
+ end
+ end
+ }.new
+
+ @migration.migrate(:up)
+
+ schema = dump_table_schema "apples_bananas"
+ assert_match %r{bigint "apple_id", null: false}, schema
+ assert_match %r{bigint "banana_id", null: false}, schema
+ end
+
if current_adapter?(:Mysql2Adapter)
def test_legacy_bigint_primary_key_should_be_auto_incremented
- @migration = Class.new(ActiveRecord::Migration[5.0]) {
+ @migration = Class.new(migration_class) {
def change
create_table :legacy_primary_keys, id: :bigint
end
@@ -210,15 +301,15 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase
@migration.migrate(:up)
legacy_pk = LegacyPrimaryKey.columns_hash["id"]
- assert legacy_pk.bigint?
- assert legacy_pk.auto_increment?
+ assert_predicate legacy_pk, :bigint?
+ assert_predicate legacy_pk, :auto_increment?
schema = dump_table_schema "legacy_primary_keys"
assert_match %r{create_table "legacy_primary_keys", (?!id: :bigint, default: nil)}, schema
end
else
def test_legacy_bigint_primary_key_should_not_be_auto_incremented
- @migration = Class.new(ActiveRecord::Migration[5.0]) {
+ @migration = Class.new(migration_class) {
def change
create_table :legacy_primary_keys, id: :bigint do |t|
end
@@ -235,4 +326,44 @@ class LegacyPrimaryKeyTest < ActiveRecord::TestCase
assert_match %r{create_table "legacy_primary_keys", id: :bigint, default: nil}, schema
end
end
+
+ private
+ def assert_legacy_primary_key
+ assert_equal "id", LegacyPrimaryKey.primary_key
+
+ legacy_pk = LegacyPrimaryKey.columns_hash["id"]
+
+ assert_equal :integer, legacy_pk.type
+ assert_not_predicate legacy_pk, :bigint?
+ assert_not legacy_pk.null
+
+ if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)
+ schema = dump_table_schema "legacy_primary_keys"
+ assert_match %r{create_table "legacy_primary_keys", id: :(?:integer|serial), (?!default: nil)}, schema
+ end
+ end
+end
+
+module LegacyPrimaryKeyTest
+ class V5_0 < ActiveRecord::TestCase
+ include LegacyPrimaryKeyTestCases
+
+ self.use_transactional_tests = false
+
+ private
+ def migration_class
+ ActiveRecord::Migration[5.0]
+ end
+ end
+
+ class V4_2 < ActiveRecord::TestCase
+ include LegacyPrimaryKeyTestCases
+
+ self.use_transactional_tests = false
+
+ private
+ def migration_class
+ ActiveRecord::Migration[4.2]
+ 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 77d32a24a5..a1e5fb1115 100644
--- a/activerecord/test/cases/migration/create_join_table_test.rb
+++ b/activerecord/test/cases/migration/create_join_table_test.rb
@@ -69,7 +69,7 @@ module ActiveRecord
def test_create_join_table_without_indexes
connection.create_join_table :artists, :musics
- assert connection.indexes(:artists_musics).blank?
+ assert_predicate connection.indexes(:artists_musics), :blank?
end
def test_create_join_table_with_index
@@ -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
diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb
index 499d072de5..50f5696ad1 100644
--- a/activerecord/test/cases/migration/foreign_key_test.rb
+++ b/activerecord/test/cases/migration/foreign_key_test.rb
@@ -227,6 +227,74 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
end
end
+ if ActiveRecord::Base.connection.supports_validate_constraints?
+ def test_add_invalid_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_not_predicate fk, :validated?
+ end
+
+ def test_validate_foreign_key_infers_column
+ @connection.add_foreign_key :astronauts, :rockets, validate: false
+ assert_not_predicate @connection.foreign_keys("astronauts").first, :validated?
+
+ @connection.validate_foreign_key :astronauts, :rockets
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+
+ def test_validate_foreign_key_by_column
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false
+ assert_not_predicate @connection.foreign_keys("astronauts").first, :validated?
+
+ @connection.validate_foreign_key :astronauts, column: "rocket_id"
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+
+ def test_validate_foreign_key_by_symbol_column
+ @connection.add_foreign_key :astronauts, :rockets, column: :rocket_id, validate: false
+ assert_not_predicate @connection.foreign_keys("astronauts").first, :validated?
+
+ @connection.validate_foreign_key :astronauts, column: :rocket_id
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+
+ def test_validate_foreign_key_by_name
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk", validate: false
+ assert_not_predicate @connection.foreign_keys("astronauts").first, :validated?
+
+ @connection.validate_foreign_key :astronauts, name: "fancy_named_fk"
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+
+ def test_validate_foreign_non_existing_foreign_key_raises
+ assert_raises ArgumentError do
+ @connection.validate_foreign_key :astronauts, :rockets
+ end
+ end
+
+ def test_validate_constraint_by_name
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk", validate: false
+
+ @connection.validate_constraint :astronauts, "fancy_named_fk"
+ assert_predicate @connection.foreign_keys("astronauts").first, :validated?
+ end
+ else
+ # Foreign key should still be created, but should not be invalid
+ def test_add_invalid_foreign_key
+ @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", validate: false
+
+ foreign_keys = @connection.foreign_keys("astronauts")
+ assert_equal 1, foreign_keys.size
+
+ fk = foreign_keys.first
+ assert_predicate fk, :validated?
+ end
+ end
+
def test_schema_dumping
@connection.add_foreign_key :astronauts, :rockets
output = dump_table_schema "astronauts"
@@ -238,6 +306,17 @@ if ActiveRecord::Base.connection.supports_foreign_keys?
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
+ def test_schema_dumping_with_custom_fk_ignore_pattern
+ original_pattern = ActiveRecord::SchemaDumper.fk_ignore_pattern
+ ActiveRecord::SchemaDumper.fk_ignore_pattern = /^ignored_/
+ @connection.add_foreign_key :astronauts, :rockets, name: :ignored_fk_astronauts_rockets
+
+ output = dump_table_schema "astronauts"
+ assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output
+
+ ActiveRecord::SchemaDumper.fk_ignore_pattern = original_pattern
+ end
+
def test_schema_dumping_on_delete_and_on_update_options
@connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade
diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb
index bf1ebdb4c5..f9b2dc0c73 100644
--- a/activerecord/test/cases/migration/index_test.rb
+++ b/activerecord/test/cases/migration/index_test.rb
@@ -33,10 +33,8 @@ module ActiveRecord
connection.add_index(table_name, [:foo], name: "old_idx")
connection.rename_index(table_name, "old_idx", "new_idx")
- assert_deprecated do
- assert_not connection.index_name_exists?(table_name, "old_idx", false)
- assert connection.index_name_exists?(table_name, "new_idx", true)
- end
+ assert_not connection.index_name_exists?(table_name, "old_idx")
+ assert connection.index_name_exists?(table_name, "new_idx")
end
def test_rename_index_too_long
@@ -101,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
@@ -133,7 +131,7 @@ 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
@@ -141,7 +139,7 @@ module ActiveRecord
assert connection.index_exists?(:testings, :foo)
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
@@ -205,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 d0066f68be..dedb5ea502 100644
--- a/activerecord/test/cases/migration/pending_migrations_test.rb
+++ b/activerecord/test/cases/migration/pending_migrations_test.rb
@@ -4,37 +4,37 @@ require "cases/helper"
module ActiveRecord
class Migration
- class PendingMigrationsTest < ActiveRecord::TestCase
- def setup
- super
- @connection = Minitest::Mock.new
- @app = Minitest::Mock.new
- conn = @connection
- @pending = Class.new(CheckPending) {
- define_method(:connection) { conn }
- }.new(@app)
- @pending.instance_variable_set :@last_check, -1 # Force checking
- end
+ if current_adapter?(:SQLite3Adapter) && !in_memory_db?
+ class PendingMigrationsTest < ActiveRecord::TestCase
+ setup do
+ file = ActiveRecord::Base.connection.raw_connection.filename
+ @conn = ActiveRecord::Base.establish_connection adapter: "sqlite3", database: ":memory:", migrations_paths: MIGRATIONS_ROOT + "/valid"
+ source_db = SQLite3::Database.new file
+ dest_db = ActiveRecord::Base.connection.raw_connection
+ backup = SQLite3::Backup.new(dest_db, "main", source_db, "main")
+ backup.step(-1)
+ backup.finish
+ end
- def teardown
- assert @connection.verify
- assert @app.verify
- super
- end
+ teardown do
+ @conn.release_connection if @conn
+ ActiveRecord::Base.establish_connection :arunit
+ end
+
+ def test_errors_if_pending
+ ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true
- def test_errors_if_pending
- ActiveRecord::Migrator.stub :needs_migration?, true do
- assert_raise ActiveRecord::PendingMigrationError do
- @pending.call(nil)
+ assert_raises ActiveRecord::PendingMigrationError do
+ CheckPending.new(Proc.new {}).call({})
end
end
- end
- def test_checks_if_supported
- @app.expect :call, nil, [:foo]
+ def test_checks_if_supported
+ ActiveRecord::SchemaMigration.create_table
+ migrator = Base.connection.migration_context
+ capture(:stdout) { migrator.migrate }
- ActiveRecord::Migrator.stub :needs_migration?, false do
- @pending.call(:foo)
+ assert_nil CheckPending.new(Proc.new {}).call({})
end
end
end
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
index dfce266253..a9deb92585 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -86,9 +86,9 @@ module ActiveRecord
rename_table :cats, :felines
assert connection.table_exists? :felines
- refute connection.table_exists? :cats
+ assert_not connection.table_exists? :cats
- primary_key_name = connection.select_values(<<-SQL.strip_heredoc, "SCHEMA")[0]
+ primary_key_name = connection.select_values(<<~SQL, "SCHEMA")[0]
SELECT c.relname
FROM pg_class c
JOIN pg_index i
@@ -107,7 +107,7 @@ module ActiveRecord
connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()"
assert_nothing_raised { rename_table :cats, :felines }
assert connection.table_exists? :felines
- refute connection.table_exists? :cats
+ assert_not connection.table_exists? :cats
ensure
connection.drop_table :cats, if_exists: true
connection.drop_table :felines, if_exists: true
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 07afa89779..1fa9a3c34a 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -71,6 +71,16 @@ 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
+ assert_deprecated do
+ ActiveRecord::Migrator.migrations_path = "db/migrate"
+ end
+ end
+
def test_migration_version_matches_component_version
assert_equal ActiveRecord::VERSION::STRING.to_f, ActiveRecord::Migration.current_version
end
@@ -78,20 +88,20 @@ class MigrationTest < ActiveRecord::TestCase
def test_migrator_versions
migrations_path = MIGRATIONS_ROOT + "/valid"
old_path = ActiveRecord::Migrator.migrations_paths
- ActiveRecord::Migrator.migrations_paths = migrations_path
+ migrator = ActiveRecord::MigrationContext.new(migrations_path)
- ActiveRecord::Migrator.up(migrations_path)
- assert_equal 3, ActiveRecord::Migrator.current_version
- assert_equal false, ActiveRecord::Migrator.needs_migration?
+ migrator.up
+ assert_equal 3, migrator.current_version
+ assert_equal false, migrator.needs_migration?
- ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid")
- assert_equal 0, ActiveRecord::Migrator.current_version
- assert_equal true, ActiveRecord::Migrator.needs_migration?
+ migrator.down
+ assert_equal 0, migrator.current_version
+ assert_equal true, migrator.needs_migration?
ActiveRecord::SchemaMigration.create!(version: 3)
- assert_equal true, ActiveRecord::Migrator.needs_migration?
+ assert_equal true, migrator.needs_migration?
ensure
- ActiveRecord::Migrator.migrations_paths = old_path
+ ActiveRecord::MigrationContext.new(old_path)
end
def test_migration_detection_without_schema_migration_table
@@ -99,28 +109,31 @@ class MigrationTest < ActiveRecord::TestCase
migrations_path = MIGRATIONS_ROOT + "/valid"
old_path = ActiveRecord::Migrator.migrations_paths
- ActiveRecord::Migrator.migrations_paths = migrations_path
+ migrator = ActiveRecord::MigrationContext.new(migrations_path)
- assert_equal true, ActiveRecord::Migrator.needs_migration?
+ assert_equal true, migrator.needs_migration?
ensure
- ActiveRecord::Migrator.migrations_paths = old_path
+ ActiveRecord::MigrationContext.new(old_path)
end
def test_any_migrations
old_path = ActiveRecord::Migrator.migrations_paths
- ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/valid"
+ migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid")
- assert ActiveRecord::Migrator.any_migrations?
+ assert_predicate migrator, :any_migrations?
- ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/empty"
+ migrator_empty = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/empty")
- assert_not ActiveRecord::Migrator.any_migrations?
+ assert_not_predicate migrator_empty, :any_migrations?
ensure
- ActiveRecord::Migrator.migrations_paths = old_path
+ ActiveRecord::MigrationContext.new(old_path)
end
def test_migration_version
- assert_nothing_raised { ActiveRecord::Migrator.run(:up, MIGRATIONS_ROOT + "/version_check", 20131219224947) }
+ migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/version_check")
+ assert_equal 0, migrator.current_version
+ migrator.up(20131219224947)
+ assert_equal 20131219224947, migrator.current_version
end
def test_create_table_with_force_true_does_not_drop_nonexisting_table
@@ -157,14 +170,14 @@ class MigrationTest < ActiveRecord::TestCase
def test_add_table_with_decimals
Person.connection.drop_table :big_numbers rescue nil
- assert !BigNumber.table_exists?
+ assert_not_predicate BigNumber, :table_exists?
GiveMeBigNumbers.up
BigNumber.reset_column_information
assert BigNumber.create(
bank_balance: 1586.43,
big_bank_balance: BigDecimal("1000234000567.95"),
- world_population: 6000000000,
+ world_population: 2**62,
my_house_population: 3,
value_of_e: BigDecimal("2.7182818284590452353602875")
)
@@ -178,10 +191,8 @@ class MigrationTest < ActiveRecord::TestCase
assert_not_nil b.my_house_population
assert_not_nil b.value_of_e
- # TODO: set world_population >= 2**62 to cover 64-bit platforms and test
- # is_a?(Bignum)
assert_kind_of Integer, b.world_population
- assert_equal 6000000000, b.world_population
+ assert_equal 2**62, b.world_population
assert_kind_of Integer, b.my_house_population
assert_equal 3, b.my_house_population
assert_kind_of BigDecimal, b.bank_balance
@@ -216,15 +227,16 @@ class MigrationTest < ActiveRecord::TestCase
def test_filtering_migrations
assert_no_column Person, :last_name
- assert !Reminder.table_exists?
+ assert_not_predicate Reminder, :table_exists?
name_filter = lambda { |migration| migration.name == "ValidPeopleHaveLastNames" }
- ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", &name_filter)
+ migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid")
+ migrator.up(&name_filter)
assert_column Person, :last_name
assert_raise(ActiveRecord::StatementInvalid) { Reminder.first }
- ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter)
+ migrator.down(&name_filter)
assert_no_column Person, :last_name
assert_raise(ActiveRecord::StatementInvalid) { Reminder.first }
@@ -382,9 +394,9 @@ class MigrationTest < ActiveRecord::TestCase
current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
migrations_path = MIGRATIONS_ROOT + "/valid"
old_path = ActiveRecord::Migrator.migrations_paths
- ActiveRecord::Migrator.migrations_paths = migrations_path
+ migrator = ActiveRecord::MigrationContext.new(migrations_path)
- ActiveRecord::Migrator.up(migrations_path)
+ migrator.up
assert_equal current_env, ActiveRecord::InternalMetadata[:environment]
original_rails_env = ENV["RAILS_ENV"]
@@ -392,16 +404,16 @@ class MigrationTest < ActiveRecord::TestCase
ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo"
new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
- refute_equal current_env, new_env
+ assert_not_equal current_env, new_env
sleep 1 # mysql by default does not store fractional seconds in the database
- ActiveRecord::Migrator.up(migrations_path)
+ migrator.up
assert_equal new_env, ActiveRecord::InternalMetadata[:environment]
ensure
- ActiveRecord::Migrator.migrations_paths = old_path
+ migrator = ActiveRecord::MigrationContext.new(old_path)
ENV["RAILS_ENV"] = original_rails_env
ENV["RACK_ENV"] = original_rack_env
- ActiveRecord::Migrator.up(migrations_path)
+ migrator.up
end
def test_internal_metadata_stores_environment_when_other_data_exists
@@ -411,14 +423,15 @@ class MigrationTest < ActiveRecord::TestCase
current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
migrations_path = MIGRATIONS_ROOT + "/valid"
old_path = ActiveRecord::Migrator.migrations_paths
- ActiveRecord::Migrator.migrations_paths = migrations_path
current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call
- ActiveRecord::Migrator.up(migrations_path)
+ migrator = ActiveRecord::MigrationContext.new(migrations_path)
+ migrator.up
assert_equal current_env, ActiveRecord::InternalMetadata[:environment]
assert_equal "bar", ActiveRecord::InternalMetadata[:foo]
ensure
- ActiveRecord::Migrator.migrations_paths = old_path
+ migrator = ActiveRecord::MigrationContext.new(old_path)
+ migrator.up
end
def test_proper_table_name_on_migration
@@ -450,7 +463,7 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_rename_table_with_prefix_and_suffix
- assert !Thing.table_exists?
+ assert_not_predicate Thing, :table_exists?
ActiveRecord::Base.table_name_prefix = "p_"
ActiveRecord::Base.table_name_suffix = "_s"
Thing.reset_table_name
@@ -471,7 +484,7 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_add_drop_table_with_prefix_and_suffix
- assert !Reminder.table_exists?
+ assert_not_predicate Reminder, :table_exists?
ActiveRecord::Base.table_name_prefix = "prefix_"
ActiveRecord::Base.table_name_suffix = "_suffix"
Reminder.reset_table_name
@@ -535,7 +548,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
@@ -678,6 +691,25 @@ class MigrationTest < ActiveRecord::TestCase
assert_no_column Person, :last_name,
"without an advisory lock, the Migrator should not make any changes, but it did."
end
+
+ def test_with_advisory_lock_raises_the_right_error_when_it_fails_to_release_lock
+ migration = Class.new(ActiveRecord::Migration::Current).new
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+ lock_id = migrator.send(:generate_migrator_advisory_lock_id)
+
+ e = assert_raises(ActiveRecord::ConcurrentMigrationError) do
+ silence_stream($stderr) do
+ migrator.send(:with_advisory_lock) do
+ ActiveRecord::Base.connection.release_advisory_lock(lock_id)
+ end
+ end
+ end
+
+ assert_match(
+ /#{ActiveRecord::ConcurrentMigrationError::RELEASE_LOCK_FAILED_MESSAGE}/,
+ e.message
+ )
+ end
end
private
@@ -790,7 +822,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
@@ -801,8 +833,15 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
t.integer :age
end
- # Adding an index fires a query every time to check if an index already exists or not
- assert_queries(3) do
+ classname = ActiveRecord::Base.connection.class.name[/[^:]*$/]
+ expected_query_count = {
+ "Mysql2Adapter" => 3, # Adding an index fires a query every time to check if an index already exists or not
+ "PostgreSQLAdapter" => 2,
+ }.fetch(classname) {
+ raise "need an expected query count for #{classname}"
+ }
+
+ assert_queries(expected_query_count) do
with_bulk_change_table do |t|
t.index :username, unique: true, name: :awesome_username_index
t.index [:name, :age]
@@ -813,7 +852,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
@@ -826,14 +865,22 @@ if ActiveRecord::Base.connection.supports_bulk_alter?
assert index(:index_delete_me_on_name)
- assert_queries(3) do
+ classname = ActiveRecord::Base.connection.class.name[/[^:]*$/]
+ expected_query_count = {
+ "Mysql2Adapter" => 3, # Adding an index fires a query every time to check if an index already exists or not
+ "PostgreSQLAdapter" => 2,
+ }.fetch(classname) {
+ raise "need an expected query count for #{classname}"
+ }
+
+ assert_queries(expected_query_count) do
with_bulk_change_table do |t|
t.remove_index :name
t.index :name, name: :new_name_index, unique: true
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
@@ -845,21 +892,27 @@ 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
- # One query for columns (delete_me table)
- # One query for primary key (delete_me table)
- # One query to do the bulk change
- assert_queries(3, ignore_none: true) do
+ classname = ActiveRecord::Base.connection.class.name[/[^:]*$/]
+ expected_query_count = {
+ "Mysql2Adapter" => 3, # one query for columns, one query for primary key, one query to do the bulk change
+ "PostgreSQLAdapter" => 3, # one query for columns, one for bulk change, one for comment
+ }.fetch(classname) {
+ raise "need an expected query count for #{classname}"
+ }
+
+ assert_queries(expected_query_count, ignore_none: true) do
with_bulk_change_table do |t|
t.change :name, :string, default: "NONAME"
- t.change :birthdate, :datetime
+ t.change :birthdate, :datetime, comment: "This is a comment"
end
end
assert_equal "NONAME", column(:name).default
assert_equal :datetime, column(:birthdate).type
+ assert_equal "This is a comment", column(:birthdate).comment
end
private
@@ -919,7 +972,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase
files_count = Dir[@migrations_path + "/*.rb"].length
copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy")
assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
- assert copied.empty?
+ assert_empty copied
ensure
clear
end
@@ -960,7 +1013,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase
files_count = Dir[@migrations_path + "/*.rb"].length
copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy_with_timestamps")
assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
- assert copied.empty?
+ assert_empty copied
end
ensure
clear
@@ -1002,7 +1055,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase
files_count = Dir[@migrations_path + "/*.rb"].length
copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/to_copy_with_timestamps")
assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
- assert copied.empty?
+ assert_empty copied
end
ensure
clear
@@ -1023,7 +1076,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase
files_count = Dir[@migrations_path + "/*.rb"].length
copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/magic")
assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
- assert copied.empty?
+ assert_empty copied
ensure
clear
end
@@ -1106,17 +1159,4 @@ class CopyMigrationsTest < ActiveRecord::TestCase
def test_unknown_migration_version_should_raise_an_argument_error
assert_raise(ArgumentError) { ActiveRecord::Migration[1.0] }
end
-
- def test_deprecate_initialize_internal_tables
- assert_deprecated { ActiveRecord::Base.connection.initialize_schema_migrations_table }
- assert_deprecated { ActiveRecord::Base.connection.initialize_internal_metadata_table }
- end
-
- def test_deprecate_supports_migrations
- assert_deprecated { ActiveRecord::Base.connection.supports_migrations? }
- end
-
- def test_deprecate_schema_migrations_table_name
- assert_deprecated { ActiveRecord::Migrator.schema_migrations_table_name }
- end
end
diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb
index ee10be119c..873455cf67 100644
--- a/activerecord/test/cases/migrator_test.rb
+++ b/activerecord/test/cases/migrator_test.rb
@@ -66,10 +66,30 @@ class MigratorTest < ActiveRecord::TestCase
list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
ActiveRecord::Migrator.new(:up, list, 3).run
end
+
+ assert_raises(ActiveRecord::UnknownMigrationVersionError) do
+ list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
+ ActiveRecord::Migrator.new(:up, list, -1).run
+ end
+
+ assert_raises(ActiveRecord::UnknownMigrationVersionError) do
+ list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
+ ActiveRecord::Migrator.new(:up, list, 0).run
+ end
+
+ assert_raises(ActiveRecord::UnknownMigrationVersionError) do
+ list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
+ ActiveRecord::Migrator.new(:up, list, 3).migrate
+ end
+
+ assert_raises(ActiveRecord::UnknownMigrationVersionError) do
+ list = [ActiveRecord::Migration.new("Foo", 1), ActiveRecord::Migration.new("Bar", 2)]
+ ActiveRecord::Migrator.new(:up, list, -1).migrate
+ end
end
def test_finds_migrations
- migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid")
+ migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid").migrations
[[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i|
assert_equal migrations[i].version, pair.first
@@ -78,7 +98,8 @@ class MigratorTest < ActiveRecord::TestCase
end
def test_finds_migrations_in_subdirectories
- migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid_with_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
@@ -88,7 +109,7 @@ class MigratorTest < ActiveRecord::TestCase
def test_finds_migrations_from_two_directories
directories = [MIGRATIONS_ROOT + "/valid_with_timestamps", MIGRATIONS_ROOT + "/to_copy_with_timestamps"]
- migrations = ActiveRecord::Migrator.migrations directories
+ migrations = ActiveRecord::MigrationContext.new(directories).migrations
[[20090101010101, "PeopleHaveHobbies"],
[20090101010202, "PeopleHaveDescriptions"],
@@ -101,14 +122,14 @@ class MigratorTest < ActiveRecord::TestCase
end
def test_finds_migrations_in_numbered_directory
- migrations = ActiveRecord::Migrator.migrations [MIGRATIONS_ROOT + "/10_urban"]
+ migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/10_urban").migrations
assert_equal 9, migrations[0].version
assert_equal "AddExpressions", migrations[0].name
end
def test_relative_migrations
list = Dir.chdir(MIGRATIONS_ROOT) do
- ActiveRecord::Migrator.migrations("valid")
+ ActiveRecord::MigrationContext.new("valid").migrations
end
migration_proxy = list.find { |item|
@@ -137,7 +158,7 @@ class MigratorTest < ActiveRecord::TestCase
["up", "002", "We need reminders"],
["down", "003", "Innocent jointable"],
["up", "010", "********** NO FILE **********"],
- ], ActiveRecord::Migrator.migrations_status(path)
+ ], ActiveRecord::MigrationContext.new(path).migrations_status
end
def test_migrations_status_in_subdirectories
@@ -151,7 +172,7 @@ class MigratorTest < ActiveRecord::TestCase
["up", "002", "We need reminders"],
["down", "003", "Innocent jointable"],
["up", "010", "********** NO FILE **********"],
- ], ActiveRecord::Migrator.migrations_status(path)
+ ], ActiveRecord::MigrationContext.new(path).migrations_status
end
def test_migrations_status_with_schema_define_in_subdirectories
@@ -166,7 +187,7 @@ class MigratorTest < ActiveRecord::TestCase
["up", "001", "Valid people have last names"],
["up", "002", "We need reminders"],
["up", "003", "Innocent jointable"],
- ], ActiveRecord::Migrator.migrations_status(path)
+ ], ActiveRecord::MigrationContext.new(path).migrations_status
ensure
ActiveRecord::Migrator.migrations_paths = prev_paths
end
@@ -184,7 +205,7 @@ class MigratorTest < ActiveRecord::TestCase
["down", "20100201010101", "Valid with timestamps we need reminders"],
["down", "20100301010101", "Valid with timestamps innocent jointable"],
["up", "20160528010101", "********** NO FILE **********"],
- ], ActiveRecord::Migrator.migrations_status(paths)
+ ], ActiveRecord::MigrationContext.new(paths).migrations_status
end
def test_migrator_interleaved_migrations
@@ -212,25 +233,28 @@ class MigratorTest < ActiveRecord::TestCase
def test_up_calls_up
migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
- ActiveRecord::Migrator.new(:up, migrations).migrate
+ migrator = ActiveRecord::Migrator.new(:up, migrations)
+ migrator.migrate
assert migrations.all?(&:went_up)
assert migrations.all? { |m| !m.went_down }
- assert_equal 2, ActiveRecord::Migrator.current_version
+ assert_equal 2, migrator.current_version
end
def test_down_calls_down
test_up_calls_up
migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)]
- ActiveRecord::Migrator.new(:down, migrations).migrate
+ migrator = ActiveRecord::Migrator.new(:down, migrations)
+ migrator.migrate
assert migrations.all? { |m| !m.went_up }
assert migrations.all?(&:went_down)
- assert_equal 0, ActiveRecord::Migrator.current_version
+ assert_equal 0, migrator.current_version
end
def test_current_version
ActiveRecord::SchemaMigration.create!(version: "1000")
- assert_equal 1000, ActiveRecord::Migrator.current_version
+ migrator = ActiveRecord::MigrationContext.new("db/migrate")
+ assert_equal 1000, migrator.current_version
end
def test_migrator_one_up
@@ -269,33 +293,36 @@ class MigratorTest < ActiveRecord::TestCase
def test_migrator_double_up
calls, migrations = sensors(3)
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ migrator = ActiveRecord::Migrator.new(:up, migrations, 1)
+ assert_equal(0, migrator.current_version)
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ migrator.migrate
assert_equal [[:up, 1]], calls
calls.clear
- ActiveRecord::Migrator.new(:up, migrations, 1).migrate
+ migrator.migrate
assert_equal [], calls
end
def test_migrator_double_down
calls, migrations = sensors(3)
+ migrator = ActiveRecord::Migrator.new(:up, migrations, 1)
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ assert_equal 0, migrator.current_version
- ActiveRecord::Migrator.new(:up, migrations, 1).run
+ migrator.run
assert_equal [[:up, 1]], calls
calls.clear
- ActiveRecord::Migrator.new(:down, migrations, 1).run
+ migrator = ActiveRecord::Migrator.new(:down, migrations, 1)
+ migrator.run
assert_equal [[:down, 1]], calls
calls.clear
- ActiveRecord::Migrator.new(:down, migrations, 1).run
+ migrator.run
assert_equal [], calls
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ assert_equal 0, migrator.current_version
end
def test_migrator_verbosity
@@ -341,78 +368,85 @@ class MigratorTest < ActiveRecord::TestCase
def test_migrator_going_down_due_to_version_target
calls, migrator = migrator_class(3)
+ migrator = migrator.new("valid")
- migrator.up("valid", 1)
+ migrator.up(1)
assert_equal [[:up, 1]], calls
calls.clear
- migrator.migrate("valid", 0)
+ migrator.migrate(0)
assert_equal [[:down, 1]], calls
calls.clear
- migrator.migrate("valid")
+ migrator.migrate
assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls
end
def test_migrator_output_when_running_multiple_migrations
_, migrator = migrator_class(3)
+ migrator = migrator.new("valid")
- result = migrator.migrate("valid")
+ result = migrator.migrate
assert_equal(3, result.count)
# Nothing migrated from duplicate run
- result = migrator.migrate("valid")
+ result = migrator.migrate
assert_equal(0, result.count)
- result = migrator.rollback("valid")
+ result = migrator.rollback
assert_equal(1, result.count)
end
def test_migrator_output_when_running_single_migration
_, migrator = migrator_class(1)
- result = migrator.run(:up, "valid", 1)
+ migrator = migrator.new("valid")
+
+ result = migrator.run(:up, 1)
assert_equal(1, result.version)
end
def test_migrator_rollback
_, migrator = migrator_class(3)
+ migrator = migrator.new("valid")
- migrator.migrate("valid")
- assert_equal(3, ActiveRecord::Migrator.current_version)
+ migrator.migrate
+ assert_equal(3, migrator.current_version)
- migrator.rollback("valid")
- assert_equal(2, ActiveRecord::Migrator.current_version)
+ migrator.rollback
+ assert_equal(2, migrator.current_version)
- migrator.rollback("valid")
- assert_equal(1, ActiveRecord::Migrator.current_version)
+ migrator.rollback
+ assert_equal(1, migrator.current_version)
- migrator.rollback("valid")
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ migrator.rollback
+ assert_equal(0, migrator.current_version)
- migrator.rollback("valid")
- assert_equal(0, ActiveRecord::Migrator.current_version)
+ migrator.rollback
+ assert_equal(0, migrator.current_version)
end
def test_migrator_db_has_no_schema_migrations_table
_, migrator = migrator_class(3)
+ migrator = migrator.new("valid")
ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true
assert_not ActiveRecord::Base.connection.table_exists?("schema_migrations")
- migrator.migrate("valid", 1)
+ migrator.migrate(1)
assert ActiveRecord::Base.connection.table_exists?("schema_migrations")
end
def test_migrator_forward
_, migrator = migrator_class(3)
- migrator.migrate("/valid", 1)
- assert_equal(1, ActiveRecord::Migrator.current_version)
+ migrator = migrator.new("/valid")
+ migrator.migrate(1)
+ assert_equal(1, migrator.current_version)
- migrator.forward("/valid", 2)
- assert_equal(3, ActiveRecord::Migrator.current_version)
+ migrator.forward(2)
+ assert_equal(3, migrator.current_version)
- migrator.forward("/valid")
- assert_equal(3, ActiveRecord::Migrator.current_version)
+ migrator.forward
+ assert_equal(3, migrator.current_version)
end
def test_only_loads_pending_migrations
@@ -420,25 +454,27 @@ class MigratorTest < ActiveRecord::TestCase
ActiveRecord::SchemaMigration.create!(version: "1")
calls, migrator = migrator_class(3)
- migrator.migrate("valid", nil)
+ migrator = migrator.new("valid")
+ migrator.migrate
assert_equal [[:up, 2], [:up, 3]], calls
end
def test_get_all_versions
_, migrator = migrator_class(3)
+ migrator = migrator.new("valid")
- migrator.migrate("valid")
- assert_equal([1, 2, 3], ActiveRecord::Migrator.get_all_versions)
+ migrator.migrate
+ assert_equal([1, 2, 3], migrator.get_all_versions)
- migrator.rollback("valid")
- assert_equal([1, 2], ActiveRecord::Migrator.get_all_versions)
+ migrator.rollback
+ assert_equal([1, 2], migrator.get_all_versions)
- migrator.rollback("valid")
- assert_equal([1], ActiveRecord::Migrator.get_all_versions)
+ migrator.rollback
+ assert_equal([1], migrator.get_all_versions)
- migrator.rollback("valid")
- assert_equal([], ActiveRecord::Migrator.get_all_versions)
+ migrator.rollback
+ assert_equal([], migrator.get_all_versions)
end
private
@@ -463,11 +499,11 @@ class MigratorTest < ActiveRecord::TestCase
def migrator_class(count)
calls, migrations = sensors(count)
- migrator = Class.new(ActiveRecord::Migrator).extend(Module.new {
- define_method(:migrations) { |paths|
+ migrator = Class.new(ActiveRecord::MigrationContext) {
+ define_method(:migrations) { |*|
migrations
}
- })
+ }
[calls, migrator]
end
end
diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb
index 59be4dc5a8..6f3903eed4 100644
--- a/activerecord/test/cases/multiparameter_attributes_test.rb
+++ b/activerecord/test/cases/multiparameter_attributes_test.rb
@@ -227,7 +227,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
- assert_equal false, topic.written_on.respond_to?(:time_zone)
+ assert_not_respond_to topic.written_on, :time_zone
end
end
@@ -242,7 +242,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
- assert_equal false, topic.written_on.respond_to?(:time_zone)
+ assert_not_respond_to topic.written_on, :time_zone
end
ensure
Topic.skip_time_zone_conversion_for_attributes = []
@@ -261,7 +261,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
topic = Topic.find(1)
topic.attributes = attributes
assert_equal Time.zone.local(2000, 1, 1, 16, 24, 0), topic.bonus_time
- assert_not topic.bonus_time.utc?
+ assert_not_predicate topic.bonus_time, :utc?
attributes = {
"written_on(1i)" => "2000", "written_on(2i)" => "", "written_on(3i)" => "",
@@ -394,6 +394,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
"written_on(4i)" => "13",
"written_on(5i)" => "55",
)
- refute_predicate topic, :written_on_came_from_user?
+ assert_not_predicate topic, :written_on_came_from_user?
end
end
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
index a2ccb603a9..32af90caef 100644
--- a/activerecord/test/cases/nested_attributes_test.rb
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -36,7 +36,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
pirate.birds_with_reject_all_blank_attributes = [{ name: "", color: "", _destroy: "0" }]
pirate.save!
- assert pirate.birds_with_reject_all_blank.empty?
+ assert_empty pirate.birds_with_reject_all_blank
end
def test_should_not_build_a_new_record_if_reject_all_blank_returns_false
@@ -44,7 +44,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
pirate.birds_with_reject_all_blank_attributes = [{ name: "", color: "" }]
pirate.save!
- assert pirate.birds_with_reject_all_blank.empty?
+ assert_empty pirate.birds_with_reject_all_blank
end
def test_should_build_a_new_record_if_reject_all_blank_does_not_return_false
@@ -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
@@ -152,7 +152,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
man = Man.create(name: "Jon")
interest = man.interests.create(topic: "the ladies")
man.update(interests_attributes: { _destroy: "1", id: interest.id })
- assert man.reload.interests.empty?
+ assert_empty man.reload.interests
end
def test_reject_if_is_not_short_circuited_if_allow_destroy_is_false
@@ -240,7 +240,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
@ship.destroy
@pirate.reload.ship_attributes = { name: "Davy Jones Gold Dagger" }
- assert !@pirate.ship.persisted?
+ assert_not_predicate @pirate.ship, :persisted?
assert_equal "Davy Jones Gold Dagger", @pirate.ship.name
end
@@ -261,7 +261,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_replace_an_existing_record_if_there_is_no_id
@pirate.reload.ship_attributes = { name: "Davy Jones Gold Dagger" }
- assert !@pirate.ship.persisted?
+ assert_not_predicate @pirate.ship, :persisted?
assert_equal "Davy Jones Gold Dagger", @pirate.ship.name
assert_equal "Nights Dirty Lightning", @ship.name
end
@@ -335,7 +335,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_also_work_with_a_HashWithIndifferentAccess
@pirate.ship_attributes = ActiveSupport::HashWithIndifferentAccess.new(id: @ship.id, name: "Davy Jones Gold Dagger")
- assert @pirate.ship.persisted?
+ assert_predicate @pirate.ship, :persisted?
assert_equal "Davy Jones Gold Dagger", @pirate.ship.name
end
@@ -350,12 +350,12 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
@pirate.attributes = { ship_attributes: { id: @ship.id, _destroy: "1" } }
- assert !@pirate.ship.destroyed?
- assert @pirate.ship.marked_for_destruction?
+ assert_not_predicate @pirate.ship, :destroyed?
+ assert_predicate @pirate.ship, :marked_for_destruction?
@pirate.save
- assert @pirate.ship.destroyed?
+ assert_predicate @pirate.ship, :destroyed?
assert_nil @pirate.reload.ship
end
@@ -424,7 +424,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
@pirate.destroy
@ship.reload.pirate_attributes = { catchphrase: "Arr" }
- assert !@ship.pirate.persisted?
+ assert_not_predicate @ship.pirate, :persisted?
assert_equal "Arr", @ship.pirate.catchphrase
end
@@ -445,7 +445,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_replace_an_existing_record_if_there_is_no_id
@ship.reload.pirate_attributes = { catchphrase: "Arr" }
- assert !@ship.pirate.persisted?
+ assert_not_predicate @ship.pirate, :persisted?
assert_equal "Arr", @ship.pirate.catchphrase
assert_equal "Aye", @pirate.catchphrase
end
@@ -550,7 +550,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
@pirate.delete
@ship.reload.attributes = { update_only_pirate_attributes: { catchphrase: "Arr" } }
- assert !@ship.update_only_pirate.persisted?
+ assert_not_predicate @ship.update_only_pirate, :persisted?
end
def test_should_update_existing_when_update_only_is_true_and_no_id_is_given
@@ -632,10 +632,10 @@ module NestedAttributesOnACollectionAssociationTests
def test_should_not_load_association_when_updating_existing_records
@pirate.reload
@pirate.send(association_setter, [{ id: @child_1.id, name: "Grace OMalley" }])
- assert ! @pirate.send(@association_name).loaded?
+ assert_not_predicate @pirate.send(@association_name), :loaded?
@pirate.save
- assert ! @pirate.send(@association_name).loaded?
+ assert_not_predicate @pirate.send(@association_name), :loaded?
assert_equal "Grace OMalley", @child_1.reload.name
end
@@ -663,7 +663,7 @@ module NestedAttributesOnACollectionAssociationTests
def test_should_not_remove_scheduled_destroys_when_loading_association
@pirate.reload
@pirate.send(association_setter, [{ id: @child_1.id, _destroy: "1" }])
- assert @pirate.send(@association_name).load_target.find { |r| r.id == @child_1.id }.marked_for_destruction?
+ assert_predicate @pirate.send(@association_name).load_target.find { |r| r.id == @child_1.id }, :marked_for_destruction?
end
def test_should_take_a_hash_with_composite_id_keys_and_assign_the_attributes_to_the_associated_models
@@ -705,10 +705,10 @@ module NestedAttributesOnACollectionAssociationTests
association_getter => { "foo" => { name: "Grace OMalley" }, "bar" => { name: "Privateers Greed" } }
}
- assert !@pirate.send(@association_name).first.persisted?
+ assert_not_predicate @pirate.send(@association_name).first, :persisted?
assert_equal "Grace OMalley", @pirate.send(@association_name).first.name
- assert !@pirate.send(@association_name).last.persisted?
+ assert_not_predicate @pirate.send(@association_name).last, :persisted?
assert_equal "Privateers Greed", @pirate.send(@association_name).last.name
end
@@ -835,7 +835,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
@@ -1091,7 +1091,7 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR
test "nested singular associations are validated" do
part = ShipPart.new(name: "Stern", ship_attributes: { name: nil })
- assert_not part.valid?
+ assert_not_predicate part, :valid?
assert_equal ["Ship name can't be blank"], part.errors.full_messages
end
end
diff --git a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
index f04c68b08f..1d26057fdc 100644
--- a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
+++ b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
@@ -63,7 +63,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
# Characterizing when :before_add callback is called
test ":before_add called for new bird when not loaded" do
- assert_not @pirate.birds_with_add.loaded?
+ assert_not_predicate @pirate.birds_with_add, :loaded?
@pirate.birds_with_add_attributes = new_bird_attributes
assert_new_bird_with_callback_called
end
@@ -80,7 +80,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
end
test ":before_add not called for identical assignment when not loaded" do
- assert_not @pirate.birds_with_add.loaded?
+ assert_not_predicate @pirate.birds_with_add, :loaded?
@pirate.birds_with_add_attributes = existing_birds_attributes
assert_callbacks_not_called
end
@@ -92,7 +92,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
end
test ":before_add not called for destroy assignment when not loaded" do
- assert_not @pirate.birds_with_add.loaded?
+ assert_not_predicate @pirate.birds_with_add, :loaded?
@pirate.birds_with_add_attributes = destroy_bird_attributes
assert_callbacks_not_called
end
@@ -111,7 +111,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
# Ensuring that the records in the association target are updated,
# whether the association is loaded before or not
test "Assignment updates records in target when not loaded" do
- assert_not @pirate.birds_with_add.loaded?
+ assert_not_predicate @pirate.birds_with_add, :loaded?
@pirate.birds_with_add_attributes = update_new_and_destroy_bird_attributes
assert_assignment_affects_records_in_target(:birds_with_add)
end
@@ -124,7 +124,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
test("Assignment updates records in target when not loaded" \
" and callback loads target") do
- assert_not @pirate.birds_with_add_load.loaded?
+ assert_not_predicate @pirate.birds_with_add_load, :loaded?
@pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes
assert_assignment_affects_records_in_target(:birds_with_add_load)
end
diff --git a/activerecord/test/cases/numeric_data_test.rb b/activerecord/test/cases/numeric_data_test.rb
index f917c8f727..14db63890e 100644
--- a/activerecord/test/cases/numeric_data_test.rb
+++ b/activerecord/test/cases/numeric_data_test.rb
@@ -19,7 +19,7 @@ class NumericDataTest < ActiveRecord::TestCase
m = NumericData.new(
bank_balance: 1586.43,
big_bank_balance: BigDecimal("1000234000567.95"),
- world_population: 6000000000,
+ world_population: 2**62,
my_house_population: 3
)
assert m.save
@@ -27,11 +27,8 @@ class NumericDataTest < ActiveRecord::TestCase
m1 = NumericData.find(m.id)
assert_not_nil m1
- # As with migration_test.rb, we should make world_population >= 2**62
- # to cover 64-bit platforms and test it is a Bignum, but the main thing
- # is that it's an Integer.
assert_kind_of Integer, m1.world_population
- assert_equal 6000000000, m1.world_population
+ assert_equal 2**62, m1.world_population
assert_kind_of Integer, m1.my_house_population
assert_equal 3, m1.my_house_population
@@ -47,7 +44,7 @@ class NumericDataTest < ActiveRecord::TestCase
m = NumericData.new(
bank_balance: 1586.43122334,
big_bank_balance: BigDecimal("234000567.952344"),
- world_population: 6000000000,
+ world_population: 2**62,
my_house_population: 3
)
assert m.save
@@ -55,11 +52,8 @@ class NumericDataTest < ActiveRecord::TestCase
m1 = NumericData.find(m.id)
assert_not_nil m1
- # As with migration_test.rb, we should make world_population >= 2**62
- # to cover 64-bit platforms and test it is a Bignum, but the main thing
- # is that it's an Integer.
assert_kind_of Integer, m1.world_population
- assert_equal 6000000000, m1.world_population
+ assert_equal 2**62, m1.world_population
assert_kind_of Integer, m1.my_house_population
assert_equal 3, m1.my_house_population
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index 6cbe18cc8c..e0c5725944 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -48,7 +48,7 @@ class PersistenceTest < ActiveRecord::TestCase
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
+ assert_not 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
@@ -59,18 +59,47 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_all_with_order_and_limit_updates_subset_only
author = authors(:david)
- assert_nothing_raised do
- assert_equal 1, author.posts_sorted_by_id_limited.size
- assert_equal 2, author.posts_sorted_by_id_limited.limit(2).to_a.size
- assert_equal 1, author.posts_sorted_by_id_limited.update_all([ "body = ?", "bulk update!" ])
- assert_equal "bulk update!", posts(:welcome).body
- assert_not_equal "bulk update!", posts(:thinking).body
- end
+ 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
def test_update_many
- topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" }, nil => {} }
+ topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } }
updated = Topic.update(topic_data.keys, topic_data.values)
assert_equal [1, 2], updated.map(&:id)
@@ -78,10 +107,33 @@ class PersistenceTest < ActiveRecord::TestCase
assert_equal "2 updated", Topic.find(2).content
end
+ def test_update_many_with_duplicated_ids
+ updated = Topic.update([1, 1, 2], [
+ { "content" => "1 duplicated" }, { "content" => "1 updated" }, { "content" => "2 updated" }
+ ])
+
+ assert_equal [1, 1, 2], updated.map(&:id)
+ assert_equal "1 updated", Topic.find(1).content
+ assert_equal "2 updated", Topic.find(2).content
+ end
+
+ def test_update_many_with_invalid_id
+ topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" }, 99999 => {} }
+
+ assert_raise(ActiveRecord::RecordNotFound) do
+ Topic.update(topic_data.keys, topic_data.values)
+ end
+
+ assert_not_equal "1 updated", Topic.find(1).content
+ assert_not_equal "2 updated", Topic.find(2).content
+ end
+
def test_class_level_update_is_affected_by_scoping
topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } }
- assert_equal [], Topic.where("1=0").scoping { Topic.update(topic_data.keys, topic_data.values) }
+ assert_raise(ActiveRecord::RecordNotFound) do
+ Topic.where("1=0").scoping { Topic.update(topic_data.keys, topic_data.values) }
+ end
assert_not_equal "1 updated", Topic.find(1).content
assert_not_equal "2 updated", Topic.find(2).content
@@ -94,27 +146,31 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_delete_all_with_joins_and_where_part_is_hash
- where_args = { toys: { name: "Bone" } }
- count = Pet.joins(:toys).where(where_args).count
+ 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 count, 1
- assert_equal count, Pet.joins(:toys).where(where_args).delete_all
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.delete_all
end
def test_delete_all_with_left_joins
- where_args = { toys: { name: "Bone" } }
- count = Pet.left_joins(:toys).where(where_args).count
+ pets = Pet.left_joins(:toys).where(toys: { name: "Bone" })
- assert_equal count, 1
- assert_equal count, Pet.left_joins(:toys).where(where_args).delete_all
+ 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
- where_args = ["toys.name = ?", "Bone"]
- count = Pet.joins(:toys).where(where_args).count
+ def test_delete_all_with_includes
+ pets = Pet.includes(:toys).where(toys: { name: "Bone" })
- assert_equal count, 1
- assert_equal count, Pet.joins(:toys).where(where_args).delete_all
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.delete_all
end
def test_increment_attribute
@@ -161,7 +217,7 @@ class PersistenceTest < ActiveRecord::TestCase
def test_destroy_all
conditions = "author_name = 'Mary'"
topics_by_mary = Topic.all.merge!(where: conditions, order: "id").to_a
- assert ! topics_by_mary.empty?
+ assert_not_empty topics_by_mary
assert_difference("Topic.count", -topics_by_mary.size) do
destroyed = Topic.where(conditions).destroy_all.sort_by(&:id)
@@ -171,23 +227,40 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_destroy_many
- clients = Client.all.merge!(order: "id").find([2, 3])
+ clients = Client.find([2, 3])
assert_difference("Client.count", -2) do
- destroyed = Client.destroy([2, 3, nil]).sort_by(&:id)
+ destroyed = Client.destroy([2, 3])
assert_equal clients, destroyed
assert destroyed.all?(&:frozen?), "destroyed clients should be frozen"
end
end
+ def test_destroy_many_with_invalid_id
+ clients = Client.find([2, 3])
+
+ assert_raise(ActiveRecord::RecordNotFound) do
+ Client.destroy([2, 3, 99999])
+ end
+
+ assert_equal clients, Client.find([2, 3])
+ end
+
def test_becomes
assert_kind_of Reply, topics(:first).becomes(Reply)
assert_equal "The First Topic", topics(:first).becomes(Reply).title
end
+ def test_becomes_after_reload_schema_from_cache
+ Reply.define_attribute_methods
+ Reply.serialize(:content) # invoke reload_schema_from_cache
+ assert_kind_of Reply, topics(:first).becomes(Reply)
+ assert_equal "The First Topic", topics(:first).becomes(Reply).title
+ end
+
def test_becomes_includes_errors
company = Company.new(name: nil)
- assert !company.valid?
+ assert_not_predicate company, :valid?
original_errors = company.errors
client = company.becomes(Client)
assert_equal original_errors.keys, client.errors.keys
@@ -304,8 +377,8 @@ class PersistenceTest < ActiveRecord::TestCase
developer.destroy
new_developer = developer.dup
new_developer.save
- assert new_developer.persisted?
- assert_not new_developer.destroyed?
+ assert_predicate new_developer, :persisted?
+ assert_not_predicate new_developer, :destroyed?
end
def test_create_many
@@ -316,12 +389,13 @@ class PersistenceTest < ActiveRecord::TestCase
def test_create_columns_not_equal_attributes
topic = Topic.instantiate(
- "attributes" => {
- "title" => "Another New Topic",
- "does_not_exist" => "test"
- }
+ "title" => "Another New Topic",
+ "does_not_exist" => "test"
)
+ topic = topic.dup # reset @new_record
assert_nothing_raised { topic.save }
+ assert_predicate topic, :persisted?
+ assert_equal "Another New Topic", topic.reload.title
end
def test_create_through_factory_with_block
@@ -368,6 +442,8 @@ class PersistenceTest < ActiveRecord::TestCase
topic_reloaded = Topic.instantiate(topic.attributes.merge("does_not_exist" => "test"))
topic_reloaded.title = "A New Topic"
assert_nothing_raised { topic_reloaded.save }
+ assert_predicate topic_reloaded, :persisted?
+ assert_equal "A New Topic", topic_reloaded.reload.title
end
def test_update_for_record_with_only_primary_key
@@ -404,6 +480,22 @@ class PersistenceTest < ActiveRecord::TestCase
assert_instance_of Reply, Reply.find(reply.id)
end
+ def test_becomes_default_sti_subclass
+ original_type = Topic.columns_hash["type"].default
+ ActiveRecord::Base.connection.change_column_default :topics, :type, "Reply"
+ Topic.reset_column_information
+
+ reply = topics(:second)
+ assert_instance_of Reply, reply
+
+ topic = reply.becomes(Topic)
+ assert_instance_of Topic, topic
+
+ ensure
+ ActiveRecord::Base.connection.change_column_default :topics, :type, original_type
+ Topic.reset_column_information
+ end
+
def test_update_after_create
klass = Class.new(Topic) do
def self.name; "Topic"; end
@@ -433,7 +525,7 @@ class PersistenceTest < ActiveRecord::TestCase
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_attributes(title: "Another New Topic") }
+ assert_queries(0) { assert topic.update(title: "Another New Topic") }
end
def test_delete
@@ -469,10 +561,18 @@ class PersistenceTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(topic.id) }
end
- def test_record_not_found_exception
+ def test_find_raises_record_not_found_exception
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(99999) }
end
+ def test_update_raises_record_not_found_exception
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.update(99999, approved: true) }
+ end
+
+ def test_destroy_raises_record_not_found_exception
+ assert_raise(ActiveRecord::RecordNotFound) { Topic.destroy(99999) }
+ end
+
def test_update_all
assert_equal Topic.count, Topic.update_all("content = 'bulk updated!'")
assert_equal "bulk updated!", Topic.find(1).content
@@ -496,17 +596,24 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_update_all_with_joins
- where_args = { toys: { name: "Bone" } }
- count = Pet.left_joins(:toys).where(where_args).count
+ pets = Pet.joins(:toys).where(toys: { name: "Bone" })
- assert_equal count, Pet.joins(:toys).where(where_args).update_all(name: "Bob")
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.update_all(name: "Bob")
end
def test_update_all_with_left_joins
- where_args = { toys: { name: "Bone" } }
- count = Pet.left_joins(:toys).where(where_args).count
+ 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 count, Pet.left_joins(:toys).where(where_args).update_all(name: "Bob")
+ assert_equal true, pets.exists?
+ assert_equal pets.count, pets.update_all(name: "Bob")
end
def test_update_all_with_non_standard_table_name
@@ -515,40 +622,65 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_delete_new_record
- client = Client.new
+ client = Client.new(name: "37signals")
client.delete
- assert client.frozen?
+ assert_predicate client, :frozen?
+
+ assert_not client.save
+ assert_raise(ActiveRecord::RecordNotSaved) { client.save! }
+
+ assert_predicate client, :frozen?
+ assert_raise(RuntimeError) { client.name = "something else" }
end
def test_delete_record_with_associations
client = Client.find(3)
client.delete
- assert client.frozen?
+ assert_predicate client, :frozen?
assert_kind_of Firm, client.firm
+
+ assert_not client.save
+ assert_raise(ActiveRecord::RecordNotSaved) { client.save! }
+
+ assert_predicate client, :frozen?
assert_raise(RuntimeError) { client.name = "something else" }
end
def test_destroy_new_record
- client = Client.new
+ client = Client.new(name: "37signals")
client.destroy
- assert client.frozen?
+ assert_predicate client, :frozen?
+
+ assert_not client.save
+ assert_raise(ActiveRecord::RecordNotSaved) { client.save! }
+
+ assert_predicate client, :frozen?
+ assert_raise(RuntimeError) { client.name = "something else" }
end
def test_destroy_record_with_associations
client = Client.find(3)
client.destroy
- assert client.frozen?
+ assert_predicate client, :frozen?
assert_kind_of Firm, client.firm
+
+ assert_not client.save
+ assert_raise(ActiveRecord::RecordNotSaved) { client.save! }
+
+ assert_predicate client, :frozen?
assert_raise(RuntimeError) { client.name = "something else" }
end
def test_update_attribute
- assert !Topic.find(1).approved?
+ assert_not_predicate Topic.find(1), :approved?
Topic.find(1).update_attribute("approved", true)
- assert Topic.find(1).approved?
+ assert_predicate Topic.find(1), :approved?
Topic.find(1).update_attribute(:approved, false)
- assert !Topic.find(1).approved?
+ assert_not_predicate Topic.find(1), :approved?
+
+ Topic.find(1).update_attribute(:change_approved_before_save, true)
+ assert_predicate Topic.find(1), :approved?
end
def test_update_attribute_for_readonly_attribute
@@ -585,14 +717,14 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_column
topic = Topic.find(1)
topic.update_column("approved", true)
- assert topic.approved?
+ assert_predicate topic, :approved?
topic.reload
- assert topic.approved?
+ assert_predicate topic, :approved?
topic.update_column(:approved, false)
- assert !topic.approved?
+ assert_not_predicate topic, :approved?
topic.reload
- assert !topic.approved?
+ assert_not_predicate topic, :approved?
end
def test_update_column_should_not_use_setter_method
@@ -679,10 +811,10 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update_columns
topic = Topic.find(1)
topic.update_columns("approved" => true, title: "Sebastian Topic")
- assert topic.approved?
+ assert_predicate topic, :approved?
assert_equal "Sebastian Topic", topic.title
topic.reload
- assert topic.approved?
+ assert_predicate topic, :approved?
assert_equal "Sebastian Topic", topic.title
end
@@ -790,54 +922,45 @@ class PersistenceTest < ActiveRecord::TestCase
def test_update
topic = Topic.find(1)
- assert !topic.approved?
+ assert_not_predicate topic, :approved?
assert_equal "The First Topic", topic.title
topic.update("approved" => true, "title" => "The First Topic Updated")
topic.reload
- assert topic.approved?
+ assert_predicate topic, :approved?
assert_equal "The First Topic Updated", topic.title
topic.update(approved: false, title: "The First Topic")
topic.reload
- assert !topic.approved?
- assert_equal "The First Topic", topic.title
- end
-
- def test_update_attributes
- topic = Topic.find(1)
- assert !topic.approved?
- assert_equal "The First Topic", topic.title
-
- topic.update_attributes("approved" => true, "title" => "The First Topic Updated")
- topic.reload
- assert topic.approved?
- assert_equal "The First Topic Updated", topic.title
-
- topic.update_attributes(approved: false, title: "The First Topic")
- topic.reload
- assert !topic.approved?
+ assert_not_predicate topic, :approved?
assert_equal "The First Topic", topic.title
error = assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do
- topic.update_attributes(id: 3, title: "Hm is it possible?")
+ topic.update(id: 3, title: "Hm is it possible?")
end
assert_not_nil error.cause
assert_not_equal "Hm is it possible?", Topic.find(3).title
- topic.update_attributes(id: 1234)
+ topic.update(id: 1234)
assert_nothing_raised { topic.reload }
assert_equal topic.title, Topic.find(1234).title
end
- def test_update_attributes_parameters
+ def test_update_attributes
+ topic = Topic.find(1)
+ assert_deprecated do
+ topic.update_attributes("title" => "The First Topic Updated")
+ end
+ end
+
+ def test_update_parameters
topic = Topic.find(1)
assert_nothing_raised do
- topic.update_attributes({})
+ topic.update({})
end
assert_raises(ArgumentError) do
- topic.update_attributes(nil)
+ topic.update(nil)
end
end
@@ -863,24 +986,10 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_update_attributes!
- Reply.validates_presence_of(:title)
reply = Reply.find(2)
- assert_equal "The Second Topic of the day", reply.title
- assert_equal "Have a nice day", reply.content
-
- reply.update_attributes!("title" => "The Second Topic of the day updated", "content" => "Have a nice evening")
- reply.reload
- assert_equal "The Second Topic of the day updated", reply.title
- assert_equal "Have a nice evening", reply.content
-
- reply.update_attributes!(title: "The Second Topic of the day", content: "Have a nice day")
- reply.reload
- assert_equal "The Second Topic of the day", reply.title
- assert_equal "Have a nice day", reply.content
-
- assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(title: nil, content: "Have a nice evening") }
- ensure
- Reply.clear_validators!
+ assert_deprecated do
+ reply.update_attributes!("title" => "The Second Topic of the day updated")
+ end
end
def test_destroyed_returns_boolean
@@ -917,7 +1026,7 @@ class PersistenceTest < ActiveRecord::TestCase
Topic.find(1).replies << should_be_destroyed_reply
topic = Topic.destroy(1)
- assert topic.destroyed?
+ assert_predicate topic, :destroyed?
assert_raise(ActiveRecord::RecordNotFound) { Topic.find(1) }
assert_raise(ActiveRecord::RecordNotFound) { Reply.find(should_be_destroyed_reply.id) }
@@ -927,7 +1036,9 @@ class PersistenceTest < ActiveRecord::TestCase
should_not_be_destroyed_reply = Reply.create("title" => "hello", "content" => "world")
Topic.find(1).replies << should_not_be_destroyed_reply
- assert_nil Topic.where("1=0").scoping { Topic.destroy(1) }
+ assert_raise(ActiveRecord::RecordNotFound) do
+ Topic.where("1=0").scoping { Topic.destroy(1) }
+ end
assert_nothing_raised { Topic.find(1) }
assert_nothing_raised { Reply.find(should_not_be_destroyed_reply.id) }
@@ -995,13 +1106,13 @@ class PersistenceTest < ActiveRecord::TestCase
def test_find_via_reload
post = Post.new
- assert post.new_record?
+ assert_predicate post, :new_record?
post.id = 1
post.reload
assert_equal "Welcome to the weblog", post.title
- assert_not post.new_record?
+ assert_not_predicate post, :new_record?
end
def test_reload_via_querycache
@@ -1049,13 +1160,18 @@ class PersistenceTest < ActiveRecord::TestCase
end
def test_reset_column_information_resets_children
- child = Class.new(Topic)
- child.new # force schema to load
+ child_class = Class.new(Topic)
+ child_class.new # force schema to load
ActiveRecord::Base.connection.add_column(:topics, :foo, :string)
Topic.reset_column_information
- assert_equal "bar", child.new(foo: :bar).foo
+ # this should redefine attribute methods
+ child_class.new
+
+ assert child_class.instance_methods.include?(:foo)
+ assert child_class.instance_methods.include?(:foo_changed?)
+ assert_equal "bar", child_class.new(foo: :bar).foo
ensure
ActiveRecord::Base.connection.remove_column(:topics, :foo)
Topic.reset_column_information
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index a36d786b40..60dac91ec9 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -156,10 +156,6 @@ class PrimaryKeysTest < ActiveRecord::TestCase
assert_nothing_raised { MixedCaseMonkey.find(1).destroy }
end
- def test_deprecate_supports_primary_key
- assert_deprecated { ActiveRecord::Base.connection.supports_primary_key? }
- end
-
def test_primary_key_returns_value_if_it_exists
klass = Class.new(ActiveRecord::Base) do
self.table_name = "developers"
@@ -211,13 +207,13 @@ class PrimaryKeysTest < ActiveRecord::TestCase
def test_serial_with_quoted_sequence_name
column = MixedCaseMonkey.columns_hash[MixedCaseMonkey.primary_key]
assert_equal "nextval('\"mixed_case_monkeys_monkeyID_seq\"'::regclass)", column.default_function
- assert column.serial?
+ assert_predicate column, :serial?
end
def test_serial_with_unquoted_sequence_name
column = Topic.columns_hash[Topic.primary_key]
assert_equal "nextval('topics_id_seq'::regclass)", column.default_function
- assert column.serial?
+ assert_predicate column, :serial?
end
end
end
@@ -302,6 +298,8 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase
assert_not column.null
assert_equal :string, column.type
assert_equal 42, column.limit
+ ensure
+ Barcode.reset_column_information
end
test "schema dump primary key includes type and options" do
@@ -432,7 +430,7 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter)
@connection.create_table(:widgets, id: @pk_type, force: true)
column = @connection.columns(:widgets).find { |c| c.name == "id" }
assert_equal :integer, column.type
- assert_not column.bigint?
+ assert_not_predicate column, :bigint?
end
test "primary key with serial/integer are automatically numbered" do
@@ -451,10 +449,10 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter)
test "primary key column type with options" do
@connection.create_table(:widgets, id: :primary_key, limit: 4, unsigned: true, force: true)
column = @connection.columns(:widgets).find { |c| c.name == "id" }
- assert column.auto_increment?
+ assert_predicate column, :auto_increment?
assert_equal :integer, column.type
- assert_not column.bigint?
- assert column.unsigned?
+ assert_not_predicate column, :bigint?
+ assert_predicate column, :unsigned?
schema = dump_table_schema "widgets"
assert_match %r{create_table "widgets", id: :integer, unsigned: true, }, schema
@@ -463,10 +461,10 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter)
test "bigint primary key with unsigned" do
@connection.create_table(:widgets, id: :bigint, unsigned: true, force: true)
column = @connection.columns(:widgets).find { |c| c.name == "id" }
- assert column.auto_increment?
+ assert_predicate column, :auto_increment?
assert_equal :integer, column.type
- assert column.bigint?
- assert column.unsigned?
+ assert_predicate column, :bigint?
+ assert_predicate column, :unsigned?
schema = dump_table_schema "widgets"
assert_match %r{create_table "widgets", id: :bigint, unsigned: true, }, schema
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 5cb537b623..393f363e37 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -82,7 +82,7 @@ class QueryCacheTest < ActiveRecord::TestCase
assert_cache :off, conn
end
- assert !ActiveRecord::Base.connection.nil?
+ assert_not_predicate ActiveRecord::Base.connection, :nil?
assert_cache :off
middleware {
@@ -283,7 +283,7 @@ class QueryCacheTest < ActiveRecord::TestCase
payload[:sql].downcase!
end
- assert_raises RuntimeError do
+ assert_raises frozen_error_class do
ActiveRecord::Base.cache do
assert_queries(1) { Task.find(1); Task.find(1) }
end
@@ -320,6 +320,17 @@ class QueryCacheTest < ActiveRecord::TestCase
end
end
+ def test_cache_is_available_when_connection_is_connected
+ conf = ActiveRecord::Base.configurations
+
+ ActiveRecord::Base.configurations = {}
+ Task.cache do
+ assert_queries(1) { Task.find(1); Task.find(1) }
+ end
+ ensure
+ ActiveRecord::Base.configurations = conf
+ end
+
def test_cache_is_available_when_using_a_not_connected_connection
skip "In-Memory DB can't test for using a not connected connection" if in_memory_db?
with_temporary_connection_pool do
@@ -327,7 +338,7 @@ class QueryCacheTest < ActiveRecord::TestCase
conf = ActiveRecord::Base.configurations["arunit"].merge("name" => "test2")
ActiveRecord::Base.connection_handler.establish_connection(conf)
Task.connection_specification_name = "test2"
- refute Task.connected?
+ assert_not_predicate Task, :connected?
Task.cache do
begin
@@ -366,7 +377,7 @@ class QueryCacheTest < ActiveRecord::TestCase
post = Post.first
Post.transaction do
- post.update_attributes(title: "rollback")
+ post.update(title: "rollback")
assert_equal 1, Post.where(title: "rollback").to_a.count
raise ActiveRecord::Rollback
end
@@ -379,7 +390,7 @@ class QueryCacheTest < ActiveRecord::TestCase
begin
Post.transaction do
- post.update_attributes(title: "rollback")
+ post.update(title: "rollback")
assert_equal 1, Post.where(title: "rollback").to_a.count
raise "broken"
end
@@ -414,24 +425,25 @@ class QueryCacheTest < ActiveRecord::TestCase
def test_query_cache_does_not_establish_connection_if_unconnected
with_temporary_connection_pool do
ActiveRecord::Base.clear_active_connections!
- refute ActiveRecord::Base.connection_handler.active_connections? # sanity check
+ assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check
middleware {
- refute ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in setup"
+ assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in setup"
}.call({})
- refute ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in cleanup"
+ assert_not ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in cleanup"
end
end
def test_query_cache_is_enabled_on_connections_established_after_middleware_runs
with_temporary_connection_pool do
ActiveRecord::Base.clear_active_connections!
- refute ActiveRecord::Base.connection_handler.active_connections? # sanity check
+ assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check
middleware {
- assert ActiveRecord::Base.connection.query_cache_enabled, "QueryCache did not get lazily enabled"
+ assert_predicate ActiveRecord::Base.connection, :query_cache_enabled
}.call({})
+ assert_not_predicate ActiveRecord::Base.connection, :query_cache_enabled
end
end
@@ -444,14 +456,23 @@ class QueryCacheTest < ActiveRecord::TestCase
assert ActiveRecord::Base.connection.query_cache_enabled
Thread.new {
- refute ActiveRecord::Base.connection_pool.query_cache_enabled
- refute ActiveRecord::Base.connection.query_cache_enabled
+ assert_not ActiveRecord::Base.connection_pool.query_cache_enabled
+ assert_not ActiveRecord::Base.connection.query_cache_enabled
}.join
}.call({})
end
end
+ def test_query_cache_is_enabled_on_all_connection_pools
+ middleware {
+ ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool|
+ assert pool.query_cache_enabled
+ assert pool.connection.query_cache_enabled
+ end
+ }.call({})
+ end
+
private
def middleware(&app)
executor = Class.new(ActiveSupport::Executor)
@@ -497,19 +518,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
@@ -553,7 +574,7 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase
assert_called(ActiveRecord::Base.connection, :clear_query_cache, times: 2) do
ActiveRecord::Base.cache do
p = Post.find(1)
- assert p.categories.any?
+ assert_predicate p.categories, :any?
p.categories.delete_all
end
end
diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb
index 59d3bbb573..92eb0c814f 100644
--- a/activerecord/test/cases/quoting_test.rb
+++ b/activerecord/test/cases/quoting_test.rb
@@ -46,27 +46,60 @@ module ActiveRecord
assert_equal t.to_s(:db), @quoter.quoted_date(t)
end
- def test_quoted_time_utc
+ def test_quoted_timestamp_utc
with_timezone_config default: :utc do
t = Time.now.change(usec: 0)
assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
end
end
- def test_quoted_time_local
+ def test_quoted_timestamp_local
with_timezone_config default: :local do
t = Time.now.change(usec: 0)
assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
end
end
- def test_quoted_time_crazy
+ def test_quoted_timestamp_crazy
with_timezone_config default: :asdfasdf do
t = Time.now.change(usec: 0)
assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
end
end
+ def test_quoted_time_utc
+ 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 ", "")
+
+ assert_equal expected, @quoter.quoted_time(t)
+ end
+ end
+
+ def test_quoted_time_local
+ with_timezone_config default: :local do
+ t = Time.now.change(usec: 0)
+
+ expected = t.change(year: 2000, month: 1, day: 1)
+ expected = expected.getlocal.to_s(:db).sub("2000-01-01 ", "")
+
+ assert_equal expected, @quoter.quoted_time(t)
+ end
+ end
+
+ def test_quoted_time_crazy
+ with_timezone_config default: :asdfasdf do
+ t = Time.now.change(usec: 0)
+
+ expected = t.change(year: 2000, month: 1, day: 1)
+ expected = expected.getlocal.to_s(:db).sub("2000-01-01 ", "")
+
+ assert_equal expected, @quoter.quoted_time(t)
+ end
+ end
+
def test_quoted_datetime_utc
with_timezone_config default: :utc do
t = Time.now.change(usec: 0).to_datetime
@@ -83,23 +116,6 @@ module ActiveRecord
end
end
- class QuotedOne
- def quoted_id
- 1
- end
- end
- class SubQuotedOne < QuotedOne
- end
- def test_quote_with_quoted_id
- assert_deprecated(/defined on \S+::QuotedOne at .*quoting_test\.rb:[0-9]/) do
- assert_equal 1, @quoter.quote(QuotedOne.new)
- end
-
- assert_deprecated(/defined on \S+::SubQuotedOne\(\S+::QuotedOne\) at .*quoting_test\.rb:[0-9]/) do
- assert_equal 1, @quoter.quote(SubQuotedOne.new)
- end
- end
-
def test_quote_nil
assert_equal "NULL", @quoter.quote(nil)
end
@@ -128,7 +144,7 @@ module ActiveRecord
end
def test_quote_bigdecimal
- bigdec = BigDecimal.new((1 << 100).to_s)
+ bigdec = BigDecimal((1 << 100).to_s)
assert_equal bigdec.to_s("F"), @quoter.quote(bigdec)
end
@@ -207,26 +223,6 @@ module ActiveRecord
obj = Class.new.new
assert_raise(TypeError) { @conn.type_cast(obj) }
end
-
- def test_type_cast_object_which_responds_to_quoted_id
- quoted_id_obj = Class.new {
- def quoted_id
- "'zomg'"
- end
-
- def id
- 10
- end
- }.new
- assert_equal 10, @conn.type_cast(quoted_id_obj)
-
- quoted_id_obj = Class.new {
- def quoted_id
- "'zomg'"
- end
- }.new
- assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) }
- end
end
class QuoteBooleanTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb
index d1b85cb4ef..059fa76132 100644
--- a/activerecord/test/cases/readonly_test.rb
+++ b/activerecord/test/cases/readonly_test.rb
@@ -16,14 +16,14 @@ class ReadOnlyTest < ActiveRecord::TestCase
def test_cant_save_readonly_record
dev = Developer.find(1)
- assert !dev.readonly?
+ assert_not_predicate dev, :readonly?
dev.readonly!
- assert dev.readonly?
+ assert_predicate dev, :readonly?
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
@@ -54,56 +54,56 @@ class ReadOnlyTest < ActiveRecord::TestCase
def test_has_many_find_readonly
post = Post.find(1)
- assert !post.comments.empty?
- assert !post.comments.any?(&:readonly?)
- assert !post.comments.to_a.any?(&:readonly?)
+ assert_not_empty post.comments
+ 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
- assert !posts(:welcome).people.find(1).readonly?
+ assert_not_predicate posts(:welcome).people.find(1), :readonly?
end
def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_first
- assert !posts(:welcome).people.first.readonly?
+ assert_not_predicate posts(:welcome).people.first, :readonly?
end
def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_last
- assert !posts(:welcome).people.last.readonly?
+ assert_not_predicate posts(:welcome).people.last, :readonly?
end
def test_readonly_scoping
Post.where("1=1").scoping do
- assert !Post.find(1).readonly?
- assert Post.readonly(true).find(1).readonly?
- assert !Post.readonly(false).find(1).readonly?
+ assert_not_predicate Post.find(1), :readonly?
+ assert_predicate Post.readonly(true).find(1), :readonly?
+ assert_not_predicate Post.readonly(false).find(1), :readonly?
end
Post.joins(" ").scoping do
- assert !Post.find(1).readonly?
- assert Post.readonly.find(1).readonly?
- assert !Post.readonly(false).find(1).readonly?
+ assert_not_predicate Post.find(1), :readonly?
+ assert_predicate Post.readonly.find(1), :readonly?
+ assert_not_predicate Post.readonly(false).find(1), :readonly?
end
# Oracle barfs on this because the join includes unqualified and
# conflicting column names
unless current_adapter?(:OracleAdapter)
Post.joins(", developers").scoping do
- assert_not Post.find(1).readonly?
- assert Post.readonly.find(1).readonly?
- assert !Post.readonly(false).find(1).readonly?
+ assert_not_predicate Post.find(1), :readonly?
+ assert_predicate Post.readonly.find(1), :readonly?
+ assert_not_predicate Post.readonly(false).find(1), :readonly?
end
end
Post.readonly(true).scoping do
- assert Post.find(1).readonly?
- assert Post.readonly.find(1).readonly?
- assert !Post.readonly(false).find(1).readonly?
+ assert_predicate Post.find(1), :readonly?
+ assert_predicate Post.readonly.find(1), :readonly?
+ assert_not_predicate Post.readonly(false).find(1), :readonly?
end
end
@@ -111,10 +111,10 @@ class ReadOnlyTest < ActiveRecord::TestCase
developer = Developer.find(1)
project = Post.find(1)
- assert !developer.projects.all_as_method.first.readonly?
- assert !developer.projects.all_as_scope.first.readonly?
+ assert_not_predicate developer.projects.all_as_method.first, :readonly?
+ assert_not_predicate developer.projects.all_as_scope.first, :readonly?
- assert !project.comments.all_as_method.first.readonly?
- assert !project.comments.all_as_scope.first.readonly?
+ assert_not_predicate project.comments.all_as_method.first, :readonly?
+ assert_not_predicate project.comments.all_as_scope.first, :readonly?
end
end
diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb
index 49170abe6f..b034fe3e3b 100644
--- a/activerecord/test/cases/reaper_test.rb
+++ b/activerecord/test/cases/reaper_test.rb
@@ -18,6 +18,7 @@ module ActiveRecord
class FakePool
attr_reader :reaped
+ attr_reader :flushed
def initialize
@reaped = false
@@ -26,20 +27,24 @@ module ActiveRecord
def reap
@reaped = true
end
+
+ def flush
+ @flushed = true
+ end
end
# 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
@@ -47,6 +52,7 @@ module ActiveRecord
Thread.pass
end
assert fp.reaped
+ assert fp.flushed
end
def test_pool_has_reaper
@@ -73,14 +79,14 @@ module ActiveRecord
end
Thread.pass while conn.nil?
- assert conn.in_use?
+ assert_predicate conn, :in_use?
child.terminate
while conn.in_use?
Thread.pass
end
- assert !conn.in_use?
+ assert_not_predicate conn, :in_use?
end
end
end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index 4cd2d3aedc..abadafbad4 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -25,7 +25,6 @@ require "models/chef"
require "models/department"
require "models/cake_designer"
require "models/drink_designer"
-require "models/mocktail_designer"
require "models/recipe"
class ReflectionTest < ActiveRecord::TestCase
@@ -67,13 +66,16 @@ class ReflectionTest < ActiveRecord::TestCase
def test_column_string_type_and_limit
assert_equal :string, @first.column_for_attribute("title").type
+ assert_equal :string, @first.column_for_attribute(:title).type
+ assert_equal :string, @first.type_for_attribute("title").type
+ assert_equal :string, @first.type_for_attribute(:title).type
assert_equal 250, @first.column_for_attribute("title").limit
end
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
@@ -82,6 +84,9 @@ class ReflectionTest < ActiveRecord::TestCase
def test_integer_columns
assert_equal :integer, @first.column_for_attribute("id").type
+ assert_equal :integer, @first.column_for_attribute(:id).type
+ assert_equal :integer, @first.type_for_attribute("id").type
+ assert_equal :integer, @first.type_for_attribute(:id).type
end
def test_non_existent_columns_return_null_object
@@ -90,6 +95,9 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal "attribute_that_doesnt_exist", column.name
assert_nil column.sql_type
assert_nil column.type
+
+ column = @first.column_for_attribute(:attribute_that_doesnt_exist)
+ assert_instance_of ActiveRecord::ConnectionAdapters::NullColumn, column
end
def test_non_existent_types_are_identity_types
@@ -99,6 +107,11 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal object, type.deserialize(object)
assert_equal object, type.cast(object)
assert_equal object, type.serialize(object)
+
+ type = @first.type_for_attribute(:attribute_that_doesnt_exist)
+ assert_equal object, type.deserialize(object)
+ assert_equal object, type.cast(object)
+ assert_equal object, type.serialize(object)
end
def test_reflection_klass_for_nested_class_name
@@ -150,7 +163,7 @@ class ReflectionTest < ActiveRecord::TestCase
expected = Pirate.reflect_on_all_associations.select { |r| r.options[:autosave] }
received = Pirate.reflect_on_all_autosave_associations
- assert !received.empty?
+ assert_not_empty received
assert_not_equal Pirate.reflect_on_all_associations.length, received.length
assert_equal expected, received
end
@@ -254,50 +267,35 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal expected, actual
end
- def test_scope_chain
- expected = [
- [Tagging.reflect_on_association(:tag).scope, Post.reflect_on_association(:first_blue_tags).scope],
- [Post.reflect_on_association(:first_taggings).scope],
- [Author.reflect_on_association(:misc_posts).scope]
- ]
- actual = assert_deprecated do
- Author.reflect_on_association(:misc_post_first_blue_tags).scope_chain
- end
- assert_equal expected, actual
-
- expected = [
- [
- Tagging.reflect_on_association(:blue_tag).scope,
- Post.reflect_on_association(:first_blue_tags_2).scope,
- Author.reflect_on_association(:misc_post_first_blue_tags_2).scope
- ],
- [],
- []
- ]
- actual = assert_deprecated do
- Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain
- end
- assert_equal expected, actual
- end
-
def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case
- @hotel = Hotel.create!
- @department = @hotel.departments.create!
- @department.chefs.create!(employable: CakeDesigner.create!)
- @department.chefs.create!(employable: DrinkDesigner.create!)
+ hotel = Hotel.create!
+ department = hotel.departments.create!
+ department.chefs.create!(employable: CakeDesigner.create!)
+ department.chefs.create!(employable: DrinkDesigner.create!)
- assert_equal 1, @hotel.cake_designers.size
- assert_equal 1, @hotel.drink_designers.size
- assert_equal 2, @hotel.chefs.size
+ assert_equal 1, hotel.cake_designers.size
+ assert_equal 1, hotel.cake_designers.count
+ assert_equal 1, hotel.drink_designers.size
+ assert_equal 1, hotel.drink_designers.count
+ assert_equal 2, hotel.chefs.size
+ assert_equal 2, hotel.chefs.count
end
def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case_and_sti
- @hotel = Hotel.create!
- @hotel.mocktail_designers << MocktailDesigner.create!
+ hotel = Hotel.create!
+ hotel.mocktail_designers << MocktailDesigner.create!
+
+ assert_equal 1, hotel.mocktail_designers.size
+ assert_equal 1, hotel.mocktail_designers.count
+ assert_equal 1, hotel.chef_lists.size
+ assert_equal 1, hotel.chef_lists.count
- assert_equal 1, @hotel.mocktail_designers.size
- assert_equal 1, @hotel.mocktail_designers.count
- assert_equal 1, @hotel.chef_lists.size
+ hotel.mocktail_designers = []
+
+ assert_equal 0, hotel.mocktail_designers.size
+ assert_equal 0, hotel.mocktail_designers.count
+ assert_equal 0, hotel.chef_lists.size
+ assert_equal 0, hotel.chef_lists.count
end
def test_scope_chain_of_polymorphic_association_does_not_leak_into_other_hmt_associations
@@ -317,12 +315,12 @@ class ReflectionTest < ActiveRecord::TestCase
end
def test_nested?
- assert !Author.reflect_on_association(:comments).nested?
- assert Author.reflect_on_association(:tags).nested?
+ assert_not_predicate Author.reflect_on_association(:comments), :nested?
+ assert_predicate Author.reflect_on_association(:tags), :nested?
# Only goes :through once, but the through_reflection is a has_and_belongs_to_many, so this is
# a nested through association
- assert Category.reflect_on_association(:post_comments).nested?
+ assert_predicate Category.reflect_on_association(:post_comments), :nested?
end
def test_association_primary_key
@@ -337,15 +335,6 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal "custom_primary_key", Author.reflect_on_association(:tags_with_primary_key).association_primary_key.to_s # nested
end
- def test_association_primary_key_type
- # Normal Association
- assert_equal :integer, Author.reflect_on_association(:posts).association_primary_key_type.type
- assert_equal :string, Author.reflect_on_association(:essay).association_primary_key_type.type
-
- # Through Association
- assert_equal :string, Author.reflect_on_association(:essay_category).association_primary_key_type.type
- end
-
def test_association_primary_key_raises_when_missing_primary_key
reflection = ActiveRecord::Reflection.create(:has_many, :edge, nil, {}, Author)
assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key }
@@ -366,42 +355,49 @@ class ReflectionTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key }
end
+ def test_type
+ assert_equal "taggable_type", Post.reflect_on_association(:taggings).type.to_s
+ assert_equal "imageable_class", Post.reflect_on_association(:images).type.to_s
+ assert_nil Post.reflect_on_association(:readers).type
+ end
+
def test_foreign_type
assert_equal "sponsorable_type", Sponsor.reflect_on_association(:sponsorable).foreign_type.to_s
assert_equal "sponsorable_type", Sponsor.reflect_on_association(:thing).foreign_type.to_s
+ assert_nil Sponsor.reflect_on_association(:sponsor_club).foreign_type
end
def test_collection_association
- assert Pirate.reflect_on_association(:birds).collection?
- assert Pirate.reflect_on_association(:parrots).collection?
+ assert_predicate Pirate.reflect_on_association(:birds), :collection?
+ assert_predicate Pirate.reflect_on_association(:parrots), :collection?
- assert !Pirate.reflect_on_association(:ship).collection?
- assert !Ship.reflect_on_association(:pirate).collection?
+ assert_not_predicate Pirate.reflect_on_association(:ship), :collection?
+ assert_not_predicate Ship.reflect_on_association(:pirate), :collection?
end
def test_default_association_validation
- assert ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm).validate?
+ assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm), :validate?
- assert !ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm).validate?
- assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm).validate?
+ assert_not_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm), :validate?
+ assert_not_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm), :validate?
end
def test_always_validate_association_if_explicit
- assert ActiveRecord::Reflection.create(:has_one, :client, nil, { validate: true }, Firm).validate?
- assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { validate: true }, Firm).validate?
- assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { validate: true }, Firm).validate?
+ assert_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, { validate: true }, Firm), :validate?
+ assert_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, { validate: true }, Firm), :validate?
+ assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { validate: true }, Firm), :validate?
end
def test_validate_association_if_autosave
- assert ActiveRecord::Reflection.create(:has_one, :client, nil, { autosave: true }, Firm).validate?
- assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { autosave: true }, Firm).validate?
- assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { autosave: true }, Firm).validate?
+ assert_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, { autosave: true }, Firm), :validate?
+ assert_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, { autosave: true }, Firm), :validate?
+ assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { autosave: true }, Firm), :validate?
end
def test_never_validate_association_if_explicit
- assert !ActiveRecord::Reflection.create(:has_one, :client, nil, { autosave: true, validate: false }, Firm).validate?
- assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, { autosave: true, validate: false }, Firm).validate?
- assert !ActiveRecord::Reflection.create(:has_many, :clients, nil, { autosave: true, validate: false }, Firm).validate?
+ assert_not_predicate ActiveRecord::Reflection.create(:has_one, :client, nil, { autosave: true, validate: false }, Firm), :validate?
+ assert_not_predicate ActiveRecord::Reflection.create(:belongs_to, :client, nil, { autosave: true, validate: false }, Firm), :validate?
+ assert_not_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { autosave: true, validate: false }, Firm), :validate?
end
def test_foreign_key
@@ -409,26 +405,15 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal "category_id", Post.reflect_on_association(:categorizations).foreign_key.to_s
end
- def test_through_reflection_scope_chain_does_not_modify_other_reflections
- orig_conds = assert_deprecated do
- Post.reflect_on_association(:first_blue_tags_2).scope_chain
- end.inspect
- assert_deprecated do
- Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain
- end
- assert_equal orig_conds, assert_deprecated {
- Post.reflect_on_association(:first_blue_tags_2).scope_chain
- }.inspect
- end
-
def test_symbol_for_class_name
assert_equal Client, Firm.reflect_on_association(:unsorted_clients_with_symbol).klass
end
def test_class_for_class_name
- assert_deprecated do
- assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { class_name: Client }, Firm), :validate?
+ error = assert_raises(ArgumentError) do
+ ActiveRecord::Reflection.create(:has_many, :clients, nil, { class_name: Client }, Firm)
end
+ assert_equal "A class was passed to `:class_name` but we are expecting a string.", error.message
end
def test_join_table
diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb
index 2696d1bb00..3f3d41980c 100644
--- a/activerecord/test/cases/relation/delegation_test.rb
+++ b/activerecord/test/cases/relation/delegation_test.rb
@@ -54,4 +54,32 @@ module ActiveRecord
Comment.all
end
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,
+ ]
+
+ def test_delegate_querying_methods
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "posts"
+ end
+
+ QUERYING_METHODS.each do |method|
+ assert_respond_to klass.all, method
+ assert_respond_to klass, method
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb
index b68b3723f6..f53ef1fe35 100644
--- a/activerecord/test/cases/relation/merging_test.rb
+++ b/activerecord/test/cases/relation/merging_test.rb
@@ -58,7 +58,7 @@ class RelationMergingTest < ActiveRecord::TestCase
def test_relation_merging_with_locks
devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2))
- assert devs.locked?
+ assert_predicate devs, :locked?
end
def test_relation_merging_with_preload
@@ -72,6 +72,16 @@ class RelationMergingTest < ActiveRecord::TestCase
assert_equal 1, comments.count
end
+ def test_relation_merging_with_left_outer_joins
+ comments = Comment.joins(:post).where(body: "Thank you for the welcome").merge(Post.left_outer_joins(:author).where(body: "Such a lovely day"))
+
+ 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
@@ -107,9 +117,9 @@ class RelationMergingTest < ActiveRecord::TestCase
def test_merging_with_from_clause
relation = Post.all
- assert relation.from_clause.empty?
+ assert_empty relation.from_clause
relation = relation.merge(Post.from("posts"))
- refute relation.from_clause.empty?
+ assert_not_empty relation.from_clause
end
end
diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb
index ad3700b73a..f82ecd4449 100644
--- a/activerecord/test/cases/relation/mutation_test.rb
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -25,7 +25,7 @@ module ActiveRecord
test "#order! with symbol prepends the table name" do
assert relation.order!(:name).equal?(relation)
node = relation.order_values.first
- assert node.ascending?
+ assert_predicate node, :ascending?
assert_equal :name, node.expr.name
assert_equal "posts", node.expr.relation.name
end
@@ -88,7 +88,7 @@ module ActiveRecord
assert relation.reorder!(:name).equal?(relation)
node = relation.order_values.first
- assert node.ascending?
+ assert_predicate node, :ascending?
assert_equal :name, node.expr.name
assert_equal "posts", node.expr.relation.name
end
@@ -137,9 +137,14 @@ module ActiveRecord
assert relation.skip_query_cache_value
end
+ test "skip_preloading!" do
+ relation.skip_preloading!
+ assert relation.skip_preloading_value
+ end
+
private
def relation
- @relation ||= Relation.new(FakeKlass, Post.arel_table, Post.predicate_builder)
+ @relation ||= Relation.new(FakeKlass)
end
end
end
diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb
index 7e418f9c7d..065819e0f1 100644
--- a/activerecord/test/cases/relation/or_test.rb
+++ b/activerecord/test/cases/relation/or_test.rb
@@ -126,5 +126,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
new file mode 100644
index 0000000000..0577e6bfdb
--- /dev/null
+++ b/activerecord/test/cases/relation/select_test.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+
+module ActiveRecord
+ class SelectTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def test_select_with_nil_agrument
+ expected = Post.select(:title).to_sql
+ assert_equal expected, Post.select(nil).select(:title).to_sql
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb
index e5eb159d36..8703d238a0 100644
--- a/activerecord/test/cases/relation/where_clause_test.rb
+++ b/activerecord/test/cases/relation/where_clause_test.rb
@@ -75,8 +75,8 @@ class ActiveRecord::Relation
end
test "a clause knows if it is empty" do
- assert WhereClause.empty.empty?
- assert_not WhereClause.new(["anything"]).empty?
+ assert_empty WhereClause.empty
+ assert_not_empty WhereClause.new(["anything"])
end
test "invert cannot handle nil" do
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index d95a54a2fe..99797528b2 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -265,7 +265,7 @@ module ActiveRecord
end
def test_where_with_decimal_for_string_column
- count = Post.where(title: BigDecimal.new(0)).count
+ count = Post.where(title: BigDecimal(0)).count
assert_equal 0, count
end
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index fd5985ffe7..0f446e06aa 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -11,30 +11,29 @@ module ActiveRecord
fixtures :posts, :comments, :authors, :author_addresses, :ratings
def test_construction
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass, table: :b)
assert_equal FakeKlass, relation.klass
assert_equal :b, relation.table
assert !relation.loaded, "relation is not loaded"
end
def test_responds_to_model_and_returns_klass
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
assert_equal FakeKlass, relation.model
end
def test_initialize_single_values
- relation = Relation.new(FakeKlass, :b, nil)
- (Relation::SINGLE_VALUE_METHODS - [:create_with, :readonly]).each do |method|
+ relation = Relation.new(FakeKlass)
+ (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method|
assert_nil relation.send("#{method}_value"), method.to_s
end
- assert_equal false, relation.readonly_value
value = relation.create_with_value
assert_equal({}, value)
assert_predicate value, :frozen?
end
def test_multi_value_initialize
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
Relation::MULTI_VALUE_METHODS.each do |method|
values = relation.send("#{method}_values")
assert_equal [], values, method.to_s
@@ -43,49 +42,49 @@ module ActiveRecord
end
def test_extensions
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
assert_equal [], relation.extensions
end
def test_empty_where_values_hash
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
assert_equal({}, relation.where_values_hash)
end
def test_has_values
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ relation = Relation.new(Post)
relation.where!(id: 10)
assert_equal({ "id" => 10 }, relation.where_values_hash)
end
def test_values_wrong_table
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ relation = Relation.new(Post)
relation.where! Comment.arel_table[:id].eq(10)
assert_equal({}, relation.where_values_hash)
end
def test_tree_is_not_traversed
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ relation = Relation.new(Post)
left = relation.table[:id].eq(10)
right = relation.table[:id].eq(10)
- combine = left.and right
+ combine = left.or(right)
relation.where! combine
assert_equal({}, relation.where_values_hash)
end
def test_scope_for_create
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
assert_equal({}, relation.scope_for_create)
end
def test_create_with_value
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ relation = Relation.new(Post)
relation.create_with_value = { hello: "world" }
assert_equal({ "hello" => "world" }, relation.scope_for_create)
end
def test_create_with_value_with_wheres
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ relation = Relation.new(Post)
assert_equal({}, relation.scope_for_create)
relation.where!(id: 10)
@@ -96,11 +95,11 @@ module ActiveRecord
end
def test_empty_scope
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
- assert relation.empty_scope?
+ relation = Relation.new(Post)
+ assert_predicate relation, :empty_scope?
relation.merge!(relation)
- assert relation.empty_scope?
+ assert_predicate relation, :empty_scope?
end
def test_bad_constants_raise_errors
@@ -110,31 +109,31 @@ module ActiveRecord
end
def test_empty_eager_loading?
- relation = Relation.new(FakeKlass, :b, nil)
- assert !relation.eager_loading?
+ relation = Relation.new(FakeKlass)
+ assert_not_predicate relation, :eager_loading?
end
def test_eager_load_values
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
relation.eager_load! :b
- assert relation.eager_loading?
+ assert_predicate relation, :eager_loading?
end
def test_references_values
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
assert_equal [], relation.references_values
relation = relation.references(:foo).references(:omg, :lol)
assert_equal ["foo", "omg", "lol"], relation.references_values
end
def test_references_values_dont_duplicate
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
relation = relation.references(:foo).references(:foo)
assert_equal ["foo"], relation.references_values
end
test "merging a hash into a relation" do
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder)
+ relation = Relation.new(Post)
relation = relation.merge where: { name: :lol }, readonly: true
assert_equal({ "name" => :lol }, relation.where_clause.to_h)
@@ -142,7 +141,7 @@ module ActiveRecord
end
test "merging an empty hash into a relation" do
- assert_equal Relation::WhereClause.empty, Relation.new(FakeKlass, :b, nil).merge({}).where_clause
+ assert_equal Relation::WhereClause.empty, Relation.new(FakeKlass).merge({}).where_clause
end
test "merging a hash with unknown keys raises" do
@@ -150,7 +149,7 @@ module ActiveRecord
end
test "merging nil or false raises" do
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
e = assert_raises(ArgumentError) do
relation = relation.merge nil
@@ -166,7 +165,7 @@ module ActiveRecord
end
test "#values returns a dup of the values" do
- relation = Relation.new(Post, Post.arel_table, Post.predicate_builder).where!(name: :foo)
+ relation = Relation.new(Post).where!(name: :foo)
values = relation.values
values[:where] = nil
@@ -174,7 +173,7 @@ module ActiveRecord
end
test "relations can be created with a values hash" do
- relation = Relation.new(FakeKlass, :b, nil, select: [:foo])
+ relation = Relation.new(FakeKlass, values: { select: [:foo] })
assert_equal [:foo], relation.select_values
end
@@ -186,13 +185,13 @@ module ActiveRecord
end
end
- relation = Relation.new(klass, :b, nil)
+ relation = Relation.new(klass)
relation.merge!(where: ["foo = ?", "bar"])
assert_equal Relation::WhereClause.new(["foo = bar"]), relation.where_clause
end
def test_merging_readonly_false
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
readonly_false_relation = relation.readonly(false)
# test merging in both directions
assert_equal false, relation.merge(readonly_false_relation).readonly_value
@@ -236,17 +235,17 @@ module ActiveRecord
def test_merge_raises_with_invalid_argument
assert_raises ArgumentError do
- relation = Relation.new(FakeKlass, :b, nil)
+ relation = Relation.new(FakeKlass)
relation.merge(true)
end
end
def test_respond_to_for_non_selected_element
post = Post.select(:title).first
- assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception"
+ assert_not_respond_to post, :body, "post should not respond_to?(:body) since invoking it raises exception"
silence_warnings { post = Post.select("'title' as post_title").first }
- assert_equal false, post.respond_to?(:title), "post should not respond_to?(:body) since invoking it raises exception"
+ assert_not_respond_to post, :title, "post should not respond_to?(:body) since invoking it raises exception"
end
def test_select_quotes_when_using_from_clause
@@ -274,18 +273,32 @@ module ActiveRecord
assert_equal({ 2 => 1, 4 => 3, 5 => 1 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count)
end
+ def test_relation_merging_keeps_joining_order
+ authors = Author.where(id: 1)
+ posts = Post.joins(:author).merge(authors)
+ comments = Comment.joins(:post).merge(posts)
+ ratings = Rating.joins(:comment).merge(comments)
+
+ assert_equal 3, ratings.count
+ end
+
class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value
def type
:string
end
+ def cast(value)
+ raise value unless value == "value from user"
+ "cast value"
+ end
+
def deserialize(value)
raise value unless value == "type cast for database"
"type cast from database"
end
def serialize(value)
- raise value unless value == "value from user"
+ raise value unless value == "cast value"
"type cast for database"
end
end
@@ -302,6 +315,14 @@ module ActiveRecord
assert_equal "type cast from database", UpdateAllTestModel.first.body
end
+ def test_skip_preloading_after_arel_has_been_generated
+ assert_nothing_raised do
+ relation = Comment.all
+ relation.arel
+ relation.skip_preloading!
+ end
+ end
+
private
def skip_if_sqlite3_version_includes_quoting_bug
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index ae1dc35bff..952d2dd5d9 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -9,6 +9,7 @@ require "models/comment"
require "models/author"
require "models/entrant"
require "models/developer"
+require "models/person"
require "models/computer"
require "models/reply"
require "models/company"
@@ -19,12 +20,13 @@ require "models/tyre"
require "models/minivan"
require "models/possession"
require "models/reader"
+require "models/category"
require "models/categorization"
require "models/edge"
+require "models/subscriber"
class RelationTest < ActiveRecord::TestCase
- fixtures :authors, :author_addresses, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments,
- :tags, :taggings, :cars, :minivans
+ fixtures :authors, :author_addresses, :topics, :entrants, :developers, :people, :companies, :developers_projects, :accounts, :categories, :categorizations, :categories_posts, :posts, :comments, :tags, :taggings, :cars, :minivans
class TopicWithCallbacks < ActiveRecord::Base
self.table_name = :topics
@@ -56,7 +58,7 @@ class RelationTest < ActiveRecord::TestCase
def test_dynamic_finder
x = Post.where("author_id = ?", 1)
- assert x.klass.respond_to?(:find_by_id), "@klass should handle dynamic finders"
+ assert_respond_to x.klass, :find_by_id
end
def test_multivalue_where
@@ -98,7 +100,7 @@ class RelationTest < ActiveRecord::TestCase
2.times { assert_equal 5, topics.to_a.size }
end
- assert topics.loaded?
+ assert_predicate topics, :loaded?
end
def test_scoped_first
@@ -108,7 +110,7 @@ class RelationTest < ActiveRecord::TestCase
2.times { assert_equal "The First Topic", topics.first.title }
end
- assert ! topics.loaded?
+ assert_not_predicate topics, :loaded?
end
def test_loaded_first
@@ -119,7 +121,7 @@ class RelationTest < ActiveRecord::TestCase
assert_equal "The First Topic", topics.first.title
end
- assert topics.loaded?
+ assert_predicate topics, :loaded?
end
def test_loaded_first_with_limit
@@ -131,7 +133,7 @@ class RelationTest < ActiveRecord::TestCase
"The Second Topic of the day"], topics.first(2).map(&:title)
end
- assert topics.loaded?
+ assert_predicate topics, :loaded?
end
def test_first_get_more_than_available
@@ -152,14 +154,14 @@ class RelationTest < ActiveRecord::TestCase
2.times { topics.to_a }
end
- assert topics.loaded?
+ assert_predicate topics, :loaded?
original_size = topics.to_a.size
Topic.create! title: "fake"
assert_queries(1) { topics.reload }
assert_equal original_size + 1, topics.size
- assert topics.loaded?
+ assert_predicate topics, :loaded?
end
def test_finding_with_subquery
@@ -195,6 +197,18 @@ class RelationTest < ActiveRecord::TestCase
assert_equal(relation.map(&:post_count).sort, subquery.values.sort)
end
+ def test_finding_with_subquery_with_eager_loading_in_from
+ relation = Comment.includes(:post).where("posts.type": "Post")
+ assert_equal relation.to_a, Comment.select("*").from(relation).to_a
+ assert_equal relation.to_a, Comment.select("subquery.*").from(relation).to_a
+ assert_equal relation.to_a, Comment.select("a.*").from(relation, :a).to_a
+ end
+
+ def test_finding_with_subquery_with_eager_loading_in_where
+ relation = Comment.includes(:post).where("posts.type": "Post")
+ assert_equal relation.sort_by(&:id), Comment.where(id: relation).sort_by(&:id)
+ end
+
def test_finding_with_conditions
assert_equal ["David"], Author.where(name: "David").map(&:name)
assert_equal ["Mary"], Author.where(["name = ?", "Mary"]).map(&:name)
@@ -238,7 +252,7 @@ class RelationTest < ActiveRecord::TestCase
end
def test_reverse_order_with_function
- topics = Topic.order("length(title)").reverse_order
+ topics = Topic.order(Arel.sql("length(title)")).reverse_order
assert_equal topics(:second).title, topics.first.title
end
@@ -248,24 +262,24 @@ class RelationTest < ActiveRecord::TestCase
end
def test_reverse_order_with_function_other_predicates
- topics = Topic.order("author_name, length(title), id").reverse_order
+ topics = Topic.order(Arel.sql("author_name, length(title), id")).reverse_order
assert_equal topics(:second).title, topics.first.title
- topics = Topic.order("length(author_name), id, length(title)").reverse_order
+ topics = Topic.order(Arel.sql("length(author_name), id, length(title)")).reverse_order
assert_equal topics(:fifth).title, topics.first.title
end
def test_reverse_order_with_multiargument_function
assert_raises(ActiveRecord::IrreversibleOrderError) do
- Topic.order("concat(author_name, title)").reverse_order
+ Topic.order(Arel.sql("concat(author_name, title)")).reverse_order
end
assert_raises(ActiveRecord::IrreversibleOrderError) do
- Topic.order("concat(lower(author_name), title)").reverse_order
+ Topic.order(Arel.sql("concat(lower(author_name), title)")).reverse_order
end
assert_raises(ActiveRecord::IrreversibleOrderError) do
- Topic.order("concat(author_name, lower(title))").reverse_order
+ Topic.order(Arel.sql("concat(author_name, lower(title))")).reverse_order
end
assert_raises(ActiveRecord::IrreversibleOrderError) do
- Topic.order("concat(lower(author_name), title, length(title)").reverse_order
+ Topic.order(Arel.sql("concat(lower(author_name), title, length(title)")).reverse_order
end
end
@@ -277,10 +291,10 @@ class RelationTest < ActiveRecord::TestCase
def test_reverse_order_with_nulls_first_or_last
assert_raises(ActiveRecord::IrreversibleOrderError) do
- Topic.order("title NULLS FIRST").reverse_order
+ Topic.order(Arel.sql("title NULLS FIRST")).reverse_order
end
assert_raises(ActiveRecord::IrreversibleOrderError) do
- Topic.order("title nulls last").reverse_order
+ Topic.order(Arel.sql("title nulls last")).reverse_order
end
end
@@ -373,29 +387,29 @@ class RelationTest < ActiveRecord::TestCase
def test_finding_with_cross_table_order_and_limit
tags = Tag.includes(:taggings).
- order("tags.name asc", "taggings.taggable_id asc", "REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").
+ order("tags.name asc", "taggings.taggable_id asc", Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")).
limit(1).to_a
assert_equal 1, tags.length
end
def test_finding_with_complex_order_and_limit
- tags = Tag.includes(:taggings).references(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").limit(1).to_a
+ tags = Tag.includes(:taggings).references(:taggings).order(Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")).limit(1).to_a
assert_equal 1, tags.length
end
def test_finding_with_complex_order
- tags = Tag.includes(:taggings).references(:taggings).order("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)").to_a
+ tags = Tag.includes(:taggings).references(:taggings).order(Arel.sql("REPLACE('abc', taggings.taggable_type, taggings.taggable_type)")).to_a
assert_equal 3, tags.length
end
def test_finding_with_sanitized_order
- query = Tag.order(["field(id, ?)", [1, 3, 2]]).to_sql
+ query = Tag.order([Arel.sql("field(id, ?)"), [1, 3, 2]]).to_sql
assert_match(/field\(id, 1,3,2\)/, query)
- query = Tag.order(["field(id, ?)", []]).to_sql
+ query = Tag.order([Arel.sql("field(id, ?)"), []]).to_sql
assert_match(/field\(id, NULL\)/, query)
- query = Tag.order(["field(id, ?)", nil]).to_sql
+ query = Tag.order([Arel.sql("field(id, ?)"), nil]).to_sql
assert_match(/field\(id, NULL\)/, query)
end
@@ -489,16 +503,16 @@ class RelationTest < ActiveRecord::TestCase
relation = Topic.all
["find_by_title", "find_by_title_and_author_name"].each do |method|
- assert_respond_to relation, method, "Topic.all should respond to #{method.inspect}"
+ assert_respond_to relation, method
end
end
def test_respond_to_class_methods_and_scopes
- assert Topic.all.respond_to?(:by_lifo)
+ assert_respond_to Topic.all, :by_lifo
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
@@ -589,7 +603,7 @@ class RelationTest < ActiveRecord::TestCase
reader = Reader.create! post_id: post.id, person_id: 1
comment = Comment.create! post_id: post.id, body: "body"
- assert !comment.respond_to?(:readers)
+ assert_not_respond_to comment, :readers
post_rel = Post.preload(:readers).joins(:readers).where(title: "Uhuu")
result_comment = Comment.joins(:post).merge(post_rel).to_a.first
@@ -710,7 +724,7 @@ class RelationTest < ActiveRecord::TestCase
def test_find_in_empty_array
authors = Author.all.where(id: [])
- assert authors.to_a.blank?
+ assert_predicate authors.to_a, :blank?
end
def test_where_with_ar_object
@@ -768,8 +782,6 @@ class RelationTest < ActiveRecord::TestCase
def test_find_all_using_where_with_relation
david = authors(:david)
- # switching the lines below would succeed in current rails
- # assert_queries(2) {
assert_queries(1) {
relation = Author.where(id: Author.where(id: david.id))
assert_equal [david], relation.to_a
@@ -808,8 +820,6 @@ class RelationTest < ActiveRecord::TestCase
def test_find_all_using_where_with_relation_and_alternate_primary_key
cool_first = minivans(:cool_first)
- # switching the lines below would succeed in current rails
- # assert_queries(2) {
assert_queries(1) {
relation = Minivan.where(minivan_id: Minivan.where(name: cool_first.name))
assert_equal [cool_first], relation.to_a
@@ -845,32 +855,6 @@ class RelationTest < ActiveRecord::TestCase
}
end
- def test_exists
- davids = Author.where(name: "David")
- assert davids.exists?
- assert davids.exists?(authors(:david).id)
- assert ! davids.exists?(authors(:mary).id)
- assert ! davids.exists?("42")
- assert ! davids.exists?(42)
- assert ! davids.exists?(davids.new.id)
-
- fake = Author.where(name: "fake author")
- assert ! fake.exists?
- assert ! fake.exists?(authors(:david).id)
- end
-
- def test_exists_uses_existing_scope
- post = authors(:david).posts.first
- authors = Author.includes(:posts).where(name: "David", posts: { id: post.id })
- assert authors.exists?(authors(:david).id)
- end
-
- def test_any_with_scope_on_hash_includes
- post = authors(:david).posts.first
- categories = Categorization.includes(author: :posts).where(posts: { id: post.id })
- assert categories.exists?
- end
-
def test_last
authors = Author.all
assert_equal authors(:bob), authors.last
@@ -881,19 +865,19 @@ class RelationTest < ActiveRecord::TestCase
# Force load
assert_equal [authors(:david)], davids.to_a
- assert davids.loaded?
+ assert_predicate davids, :loaded?
assert_difference("Author.count", -1) { davids.destroy_all }
assert_equal [], davids.to_a
- assert davids.loaded?
+ assert_predicate davids, :loaded?
end
def test_delete_all
davids = Author.where(name: "David")
assert_difference("Author.count", -1) { davids.delete_all }
- assert ! davids.loaded?
+ assert_not_predicate davids, :loaded?
end
def test_delete_all_loaded
@@ -901,20 +885,18 @@ class RelationTest < ActiveRecord::TestCase
# Force load
assert_equal [authors(:david)], davids.to_a
- assert davids.loaded?
+ assert_predicate davids, :loaded?
assert_difference("Author.count", -1) { davids.delete_all }
assert_equal [], davids.to_a
- assert davids.loaded?
+ assert_predicate davids, :loaded?
end
def test_delete_all_with_unpermitted_relation_raises_error
- assert_raises(ActiveRecord::ActiveRecordError) { Author.limit(10).delete_all }
assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all }
assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all }
assert_raises(ActiveRecord::ActiveRecordError) { Author.having("SUM(id) < 3").delete_all }
- assert_raises(ActiveRecord::ActiveRecordError) { Author.offset(10).delete_all }
end
def test_select_with_aggregates
@@ -922,9 +904,9 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 11, posts.count(:all)
assert_equal 11, posts.size
- assert posts.any?
- assert posts.many?
- assert_not posts.empty?
+ assert_predicate posts, :any?
+ assert_predicate posts, :many?
+ assert_not_empty posts
end
def test_select_takes_a_variable_list_of_args
@@ -953,13 +935,13 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 11, posts.count(:all)
assert_equal 11, posts.count(:id)
- assert_equal 1, posts.where("comments_count > 1").count
- assert_equal 9, posts.where(comments_count: 0).count
+ assert_equal 3, posts.where("comments_count > 1").count
+ assert_equal 6, posts.where(comments_count: 0).count
end
def test_count_with_block
posts = Post.all
- assert_equal 10, posts.count { |p| p.comments_count.even? }
+ assert_equal 8, posts.count { |p| p.comments_count.even? }
end
def test_count_on_association_relation
@@ -970,19 +952,37 @@ class RelationTest < ActiveRecord::TestCase
assert_equal author.posts.where(author_id: author.id).size, posts.count
assert_equal 0, author.posts.where(author_id: another_author.id).size
- assert author.posts.where(author_id: another_author.id).empty?
+ assert_empty author.posts.where(author_id: another_author.id)
end
def test_count_with_distinct
posts = Post.all
- assert_equal 3, posts.distinct(true).count(:comments_count)
+ assert_equal 4, posts.distinct(true).count(:comments_count)
assert_equal 11, posts.distinct(false).count(:comments_count)
- assert_equal 3, posts.distinct(true).select(:comments_count).count
+ assert_equal 4, posts.distinct(true).select(:comments_count).count
assert_equal 11, posts.distinct(false).select(:comments_count).count
end
+ def test_size_with_distinct
+ posts = Post.distinct.select(:author_id, :comments_count)
+ assert_queries(1) { assert_equal 8, posts.size }
+ assert_queries(1) { assert_equal 8, posts.load.size }
+ end
+
+ def test_size_with_eager_loading_and_custom_order
+ posts = Post.includes(:comments).order("comments.id")
+ 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"
@@ -1014,29 +1014,29 @@ class RelationTest < ActiveRecord::TestCase
posts = Post.all
assert_queries(1) { assert_equal 11, posts.size }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
best_posts = posts.where(comments_count: 0)
best_posts.load # force load
- assert_no_queries { assert_equal 9, best_posts.size }
+ assert_no_queries { assert_equal 6, best_posts.size }
end
def test_size_with_limit
posts = Post.limit(10)
assert_queries(1) { assert_equal 10, posts.size }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
best_posts = posts.where(comments_count: 0)
best_posts.load # force load
- assert_no_queries { assert_equal 9, best_posts.size }
+ assert_no_queries { assert_equal 6, best_posts.size }
end
def test_size_with_zero_limit
posts = Post.limit(0)
assert_no_queries { assert_equal 0, posts.size }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
posts.load # force load
assert_no_queries { assert_equal 0, posts.size }
@@ -1046,13 +1046,13 @@ class RelationTest < ActiveRecord::TestCase
posts = Post.limit(0)
assert_no_queries { assert_equal true, posts.empty? }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
end
def test_count_complex_chained_relations
posts = Post.select("comments_count").where("id is not null").group("author_id").where("comments_count > 0")
- expected = { 1 => 2 }
+ expected = { 1 => 4, 2 => 1 }
assert_equal expected, posts.count
end
@@ -1060,11 +1060,11 @@ class RelationTest < ActiveRecord::TestCase
posts = Post.all
assert_queries(1) { assert_equal false, posts.empty? }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
no_posts = posts.where(title: "")
assert_queries(1) { assert_equal true, no_posts.empty? }
- assert ! no_posts.loaded?
+ assert_not_predicate no_posts, :loaded?
best_posts = posts.where(comments_count: 0)
best_posts.load # force load
@@ -1075,11 +1075,11 @@ class RelationTest < ActiveRecord::TestCase
posts = Post.select("comments_count").where("id is not null").group("author_id").where("comments_count > 0")
assert_queries(1) { assert_equal false, posts.empty? }
- assert ! posts.loaded?
+ assert_not_predicate posts, :loaded?
no_posts = posts.where(title: "")
assert_queries(1) { assert_equal true, no_posts.empty? }
- assert ! no_posts.loaded?
+ assert_not_predicate no_posts, :loaded?
end
def test_any
@@ -1095,13 +1095,13 @@ class RelationTest < ActiveRecord::TestCase
assert_queries(3) do
assert posts.any? # Uses COUNT()
- assert ! posts.where(id: nil).any?
+ 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 posts.loaded?
+ assert_predicate posts, :loaded?
end
def test_many
@@ -1110,49 +1110,49 @@ 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 posts.loaded?
+ assert_predicate posts, :loaded?
end
def test_many_with_limits
posts = Post.all
- assert posts.many?
- assert ! posts.limit(1).many?
+ assert_predicate posts, :many?
+ assert_not_predicate posts.limit(1), :many?
end
def test_none?
posts = Post.all
assert_queries(1) do
- assert ! posts.none? # Uses COUNT()
+ assert_not posts.none? # Uses COUNT()
end
- assert ! posts.loaded?
+ 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 posts.loaded?
+ assert_predicate posts, :loaded?
end
def test_one
posts = Post.all
assert_queries(1) do
- assert ! posts.one? # Uses COUNT()
+ assert_not posts.one? # Uses COUNT()
end
- assert ! posts.loaded?
+ 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
- assert posts.loaded?
+ assert_predicate posts, :loaded?
end
def test_to_a_should_dup_target
@@ -1185,10 +1185,10 @@ class RelationTest < ActiveRecord::TestCase
sparrow = birds.create
assert_kind_of Bird, sparrow
- assert !sparrow.persisted?
+ assert_not_predicate sparrow, :persisted?
hen = birds.where(name: "hen").create
- assert hen.persisted?
+ assert_predicate hen, :persisted?
assert_equal "hen", hen.name
end
@@ -1199,34 +1199,43 @@ class RelationTest < ActiveRecord::TestCase
hen = birds.where(name: "hen").create!
assert_kind_of Bird, hen
- assert hen.persisted?
+ assert_predicate hen, :persisted?
assert_equal "hen", hen.name
end
+ def test_create_with_polymorphic_association
+ author = authors(:david)
+ post = posts(:welcome)
+ comment = Comment.where(post: post, author: author).create!(body: "hello")
+
+ assert_equal author, comment.author
+ assert_equal post, comment.post
+ end
+
def test_first_or_create
parrot = Bird.where(color: "green").first_or_create(name: "parrot")
assert_kind_of Bird, parrot
- assert parrot.persisted?
+ assert_predicate parrot, :persisted?
assert_equal "parrot", parrot.name
assert_equal "green", parrot.color
same_parrot = Bird.where(color: "green").first_or_create(name: "parakeet")
assert_kind_of Bird, same_parrot
- assert same_parrot.persisted?
+ assert_predicate same_parrot, :persisted?
assert_equal parrot, same_parrot
end
def test_first_or_create_with_no_parameters
parrot = Bird.where(color: "green").first_or_create
assert_kind_of Bird, parrot
- assert !parrot.persisted?
+ assert_not_predicate parrot, :persisted?
assert_equal "green", parrot.color
end
def test_first_or_create_with_block
parrot = Bird.where(color: "green").first_or_create { |bird| bird.name = "parrot" }
assert_kind_of Bird, parrot
- assert parrot.persisted?
+ assert_predicate parrot, :persisted?
assert_equal "green", parrot.color
assert_equal "parrot", parrot.name
@@ -1247,13 +1256,13 @@ class RelationTest < ActiveRecord::TestCase
def test_first_or_create_bang_with_valid_options
parrot = Bird.where(color: "green").first_or_create!(name: "parrot")
assert_kind_of Bird, parrot
- assert parrot.persisted?
+ assert_predicate parrot, :persisted?
assert_equal "parrot", parrot.name
assert_equal "green", parrot.color
same_parrot = Bird.where(color: "green").first_or_create!(name: "parakeet")
assert_kind_of Bird, same_parrot
- assert same_parrot.persisted?
+ assert_predicate same_parrot, :persisted?
assert_equal parrot, same_parrot
end
@@ -1268,7 +1277,7 @@ class RelationTest < ActiveRecord::TestCase
def test_first_or_create_bang_with_valid_block
parrot = Bird.where(color: "green").first_or_create! { |bird| bird.name = "parrot" }
assert_kind_of Bird, parrot
- assert parrot.persisted?
+ assert_predicate parrot, :persisted?
assert_equal "green", parrot.color
assert_equal "parrot", parrot.name
@@ -1299,9 +1308,9 @@ class RelationTest < ActiveRecord::TestCase
def test_first_or_initialize
parrot = Bird.where(color: "green").first_or_initialize(name: "parrot")
assert_kind_of Bird, parrot
- assert !parrot.persisted?
- assert parrot.valid?
- assert parrot.new_record?
+ assert_not_predicate parrot, :persisted?
+ assert_predicate parrot, :valid?
+ assert_predicate parrot, :new_record?
assert_equal "parrot", parrot.name
assert_equal "green", parrot.color
end
@@ -1309,18 +1318,18 @@ class RelationTest < ActiveRecord::TestCase
def test_first_or_initialize_with_no_parameters
parrot = Bird.where(color: "green").first_or_initialize
assert_kind_of Bird, parrot
- assert !parrot.persisted?
- assert !parrot.valid?
- assert parrot.new_record?
+ assert_not_predicate parrot, :persisted?
+ assert_not_predicate parrot, :valid?
+ assert_predicate parrot, :new_record?
assert_equal "green", parrot.color
end
def test_first_or_initialize_with_block
parrot = Bird.where(color: "green").first_or_initialize { |bird| bird.name = "parrot" }
assert_kind_of Bird, parrot
- assert !parrot.persisted?
- assert parrot.valid?
- assert parrot.new_record?
+ assert_not_predicate parrot, :persisted?
+ assert_predicate parrot, :valid?
+ assert_predicate parrot, :new_record?
assert_equal "green", parrot.color
assert_equal "parrot", parrot.name
end
@@ -1329,7 +1338,7 @@ class RelationTest < ActiveRecord::TestCase
assert_nil Bird.find_by(name: "bob")
bird = Bird.find_or_create_by(name: "bob")
- assert bird.persisted?
+ assert_predicate bird, :persisted?
assert_equal bird, Bird.find_or_create_by(name: "bob")
end
@@ -1338,7 +1347,7 @@ class RelationTest < ActiveRecord::TestCase
assert_nil Bird.find_by(name: "bob")
bird = Bird.create_with(color: "green").find_or_create_by(name: "bob")
- assert bird.persisted?
+ assert_predicate bird, :persisted?
assert_equal "green", bird.color
assert_equal bird, Bird.create_with(color: "blue").find_or_create_by(name: "bob")
@@ -1348,11 +1357,39 @@ class RelationTest < ActiveRecord::TestCase
assert_raises(ActiveRecord::RecordInvalid) { Bird.find_or_create_by!(color: "green") }
end
+ def test_create_or_find_by
+ 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_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_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")
bird = Bird.find_or_initialize_by(name: "bob")
- assert bird.new_record?
+ assert_predicate bird, :new_record?
bird.save!
assert_equal bird, Bird.find_or_initialize_by(name: "bob")
@@ -1489,6 +1526,50 @@ class RelationTest < ActiveRecord::TestCase
assert_equal posts(:welcome), comments(:greetings).post
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 = 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 = 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_touch_all_updates_locking_column
+ person = people(:david)
+
+ assert_difference -> { person.reload.lock_version }, +1 do
+ Person.where(first_name: "David").touch_all
+ end
+ end
+
def test_update_on_relation
topic1 = TopicWithCallbacks.create! title: "arel", author_name: nil
topic2 = TopicWithCallbacks.create! title: "activerecord", author_name: nil
@@ -1527,10 +1608,10 @@ class RelationTest < ActiveRecord::TestCase
def test_doesnt_add_having_values_if_options_are_blank
scope = Post.having("")
- assert scope.having_clause.empty?
+ assert_empty scope.having_clause
scope = Post.having([])
- assert scope.having_clause.empty?
+ assert_empty scope.having_clause
end
def test_having_with_binds_for_both_where_and_having
@@ -1556,13 +1637,13 @@ class RelationTest < ActiveRecord::TestCase
def test_references_triggers_eager_loading
scope = Post.includes(:comments)
- assert !scope.eager_loading?
- assert scope.references(:comments).eager_loading?
+ assert_not_predicate scope, :eager_loading?
+ assert_predicate scope.references(:comments), :eager_loading?
end
def test_references_doesnt_trigger_eager_loading_if_reference_not_included
scope = Post.references(:comments)
- assert !scope.eager_loading?
+ assert_not_predicate scope, :eager_loading?
end
def test_automatically_added_where_references
@@ -1593,7 +1674,7 @@ class RelationTest < ActiveRecord::TestCase
scope = Post.order("comments.body")
assert_equal ["comments"], scope.references_values
- scope = Post.order("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}")
+ scope = Post.order(Arel.sql("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}"))
if current_adapter?(:OracleAdapter)
assert_equal ["COMMENTS"], scope.references_values
else
@@ -1610,7 +1691,7 @@ class RelationTest < ActiveRecord::TestCase
scope = Post.order("comments.body asc")
assert_equal ["comments"], scope.references_values
- scope = Post.order("foo(comments.body)")
+ scope = Post.order(Arel.sql("foo(comments.body)"))
assert_equal [], scope.references_values
end
@@ -1618,7 +1699,7 @@ class RelationTest < ActiveRecord::TestCase
scope = Post.reorder("comments.body")
assert_equal %w(comments), scope.references_values
- scope = Post.reorder("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}")
+ scope = Post.reorder(Arel.sql("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}"))
if current_adapter?(:OracleAdapter)
assert_equal ["COMMENTS"], scope.references_values
else
@@ -1635,7 +1716,7 @@ class RelationTest < ActiveRecord::TestCase
scope = Post.reorder("comments.body asc")
assert_equal %w(comments), scope.references_values
- scope = Post.reorder("foo(comments.body)")
+ scope = Post.reorder(Arel.sql("foo(comments.body)"))
assert_equal [], scope.references_values
end
@@ -1660,7 +1741,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 }
@@ -1670,7 +1751,7 @@ class RelationTest < ActiveRecord::TestCase
# count always trigger the COUNT query.
assert_queries(1) { topics.count }
- assert topics.loaded?
+ assert_predicate topics, :loaded?
end
test "find_by with hash conditions returns the first matching record" do
@@ -1806,6 +1887,14 @@ class RelationTest < ActiveRecord::TestCase
assert_equal post, custom_post_relation.joins(:author).where!(title: post.title).take
end
+ test "arel_attribute respects a custom table" do
+ assert_equal [posts(:sti_comments)], custom_post_relation.ranked_by_comments.limit_by(1).to_a
+ end
+
+ test "alias_tracker respects a custom table" do
+ assert_equal posts(:welcome), custom_post_relation("categories_posts").joins(:categories).first
+ end
+
test "#load" do
relation = Post.all
assert_queries(1) do
@@ -1837,7 +1926,7 @@ class RelationTest < ActiveRecord::TestCase
test "delegations do not leak to other classes" do
Topic.all.by_lifo
assert Topic.all.class.method_defined?(:by_lifo)
- assert !Post.all.respond_to?(:by_lifo)
+ assert_not_respond_to Post.all, :by_lifo
end
def test_unscope_with_subquery
@@ -1862,7 +1951,7 @@ class RelationTest < ActiveRecord::TestCase
def test_locked_should_not_build_arel
posts = Post.locked
- assert posts.locked?
+ assert_predicate posts, :locked?
assert_nothing_raised { posts.lock!(false) }
end
@@ -1912,12 +2001,29 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ test "#where with set" do
+ david = authors(:david)
+ mary = authors(:mary)
+
+ authors = Author.where(name: ["David", "Mary"].to_set)
+ assert_equal [david, mary], authors
+ end
+
+ test "#where with empty set" do
+ authors = Author.where(name: Set.new)
+ assert_empty authors
+ end
+
private
- def custom_post_relation
- table_alias = Post.arel_table.alias("omg_posts")
+ def custom_post_relation(alias_name = "omg_posts")
+ table_alias = Post.arel_table.alias(alias_name)
table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias)
predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata)
- ActiveRecord::Relation.create(Post, table_alias, predicate_builder)
+ ActiveRecord::Relation.create(
+ Post,
+ table: table_alias,
+ predicate_builder: predicate_builder
+ )
end
end
diff --git a/activerecord/test/cases/reserved_word_test.rb b/activerecord/test/cases/reserved_word_test.rb
index 0214dbec17..e32605fd11 100644
--- a/activerecord/test/cases/reserved_word_test.rb
+++ b/activerecord/test/cases/reserved_word_test.rb
@@ -39,7 +39,7 @@ class ReservedWordTest < ActiveRecord::TestCase
t.string :order
t.belongs_to :select
end
- @connection.create_table :values, force: true do |t|
+ @connection.create_table :values, primary_key: :as, force: true do |t|
t.belongs_to :group
end
end
@@ -88,6 +88,13 @@ class ReservedWordTest < ActiveRecord::TestCase
assert_equal x, Group.find(x.id)
end
+ def test_delete_all_with_subselect
+ create_test_fixtures :values
+ assert_equal 1, Values.order(:as).limit(1).offset(1).delete_all
+ assert_raise(ActiveRecord::RecordNotFound) { Values.find(2) }
+ assert Values.find(1)
+ end
+
def test_has_one_associations
create_test_fixtures :group, :values
v = Group.find(1).values
@@ -109,7 +116,7 @@ class ReservedWordTest < ActiveRecord::TestCase
end
def test_activerecord_introspection
- assert Group.table_exists?
+ assert_predicate Group, :table_exists?
assert_equal ["id", "order", "select_id"], Group.columns.map(&:name).sort
end
diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb
index 082d663675..778cf86ac3 100644
--- a/activerecord/test/cases/sanitize_test.rb
+++ b/activerecord/test/cases/sanitize_test.rb
@@ -4,6 +4,7 @@ require "cases/helper"
require "models/binary"
require "models/author"
require "models/post"
+require "models/customer"
class SanitizeTest < ActiveRecord::TestCase
def setup
@@ -11,30 +12,30 @@ class SanitizeTest < ActiveRecord::TestCase
def test_sanitize_sql_array_handles_string_interpolation
quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi")
- assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi"])
- assert_equal "name='#{quoted_bambi}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi".mb_chars])
+ assert_equal "name='#{quoted_bambi}'", Binary.sanitize_sql_array(["name='%s'", "Bambi"])
+ assert_equal "name='#{quoted_bambi}'", Binary.sanitize_sql_array(["name='%s'", "Bambi".mb_chars])
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote_string("Bambi\nand\nThumper")
- assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper"])
- assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.send(:sanitize_sql_array, ["name='%s'", "Bambi\nand\nThumper".mb_chars])
+ assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.sanitize_sql_array(["name='%s'", "Bambi\nand\nThumper"])
+ assert_equal "name='#{quoted_bambi_and_thumper}'", Binary.sanitize_sql_array(["name='%s'", "Bambi\nand\nThumper".mb_chars])
end
def test_sanitize_sql_array_handles_bind_variables
quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi"])
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi".mb_chars])
+ assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=?", "Bambi"])
+ assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=?", "Bambi".mb_chars])
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper")
- assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper"])
- assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars])
+ assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=?", "Bambi\nand\nThumper"])
+ assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=?", "Bambi\nand\nThumper".mb_chars])
end
def test_sanitize_sql_array_handles_named_bind_variables
quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
- assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=:name", name: "Bambi"])
- assert_equal "name=#{quoted_bambi} AND id=1", Binary.send(:sanitize_sql_array, ["name=:name AND id=:id", name: "Bambi", id: 1])
+ assert_equal "name=#{quoted_bambi}", Binary.sanitize_sql_array(["name=:name", name: "Bambi"])
+ assert_equal "name=#{quoted_bambi} AND id=1", Binary.sanitize_sql_array(["name=:name AND id=:id", name: "Bambi", id: 1])
quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper")
- assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=:name", name: "Bambi\nand\nThumper"])
- assert_equal "name=#{quoted_bambi_and_thumper} AND name2=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=:name AND name2=:name", name: "Bambi\nand\nThumper"])
+ assert_equal "name=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=:name", name: "Bambi\nand\nThumper"])
+ assert_equal "name=#{quoted_bambi_and_thumper} AND name2=#{quoted_bambi_and_thumper}", Binary.sanitize_sql_array(["name=:name AND name2=:name", name: "Bambi\nand\nThumper"])
end
def test_sanitize_sql_array_handles_relations
@@ -43,42 +44,50 @@ class SanitizeTest < ActiveRecord::TestCase
sub_query_pattern = /\(\bselect\b.*?\bwhere\b.*?\)/i
- select_author_sql = Post.send(:sanitize_sql_array, ["id in (?)", david_posts])
+ select_author_sql = Post.sanitize_sql_array(["id in (?)", david_posts])
assert_match(sub_query_pattern, select_author_sql, "should sanitize `Relation` as subquery for bind variables")
- select_author_sql = Post.send(:sanitize_sql_array, ["id in (:post_ids)", post_ids: david_posts])
+ select_author_sql = Post.sanitize_sql_array(["id in (:post_ids)", post_ids: david_posts])
assert_match(sub_query_pattern, select_author_sql, "should sanitize `Relation` as subquery for named bind variables")
end
def test_sanitize_sql_array_handles_empty_statement
- select_author_sql = Post.send(:sanitize_sql_array, [""])
+ select_author_sql = Post.sanitize_sql_array([""])
assert_equal("", select_author_sql)
end
def test_sanitize_sql_like
- assert_equal '100\%', Binary.send(:sanitize_sql_like, "100%")
- assert_equal 'snake\_cased\_string', Binary.send(:sanitize_sql_like, "snake_cased_string")
- assert_equal 'C:\\\\Programs\\\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint')
- assert_equal "normal string 42", Binary.send(:sanitize_sql_like, "normal string 42")
+ assert_equal '100\%', Binary.sanitize_sql_like("100%")
+ assert_equal 'snake\_cased\_string', Binary.sanitize_sql_like("snake_cased_string")
+ assert_equal 'C:\\\\Programs\\\\MsPaint', Binary.sanitize_sql_like('C:\\Programs\\MsPaint')
+ assert_equal "normal string 42", Binary.sanitize_sql_like("normal string 42")
end
def test_sanitize_sql_like_with_custom_escape_character
- assert_equal "100!%", Binary.send(:sanitize_sql_like, "100%", "!")
- assert_equal "snake!_cased!_string", Binary.send(:sanitize_sql_like, "snake_cased_string", "!")
- assert_equal "great!!", Binary.send(:sanitize_sql_like, "great!", "!")
- assert_equal 'C:\\Programs\\MsPaint', Binary.send(:sanitize_sql_like, 'C:\\Programs\\MsPaint', "!")
- assert_equal "normal string 42", Binary.send(:sanitize_sql_like, "normal string 42", "!")
+ assert_equal "100!%", Binary.sanitize_sql_like("100%", "!")
+ assert_equal "snake!_cased!_string", Binary.sanitize_sql_like("snake_cased_string", "!")
+ assert_equal "great!!", Binary.sanitize_sql_like("great!", "!")
+ assert_equal 'C:\\Programs\\MsPaint', Binary.sanitize_sql_like('C:\\Programs\\MsPaint', "!")
+ assert_equal "normal string 42", Binary.sanitize_sql_like("normal string 42", "!")
end
def test_sanitize_sql_like_example_use_case
searchable_post = Class.new(Post) do
- def self.search(term)
+ def self.search_as_method(term)
where("title LIKE ?", sanitize_sql_like(term, "!"))
end
+
+ scope :search_as_scope, -> (term) {
+ where("title LIKE ?", sanitize_sql_like(term, "!"))
+ }
+ end
+
+ assert_sql(/LIKE '20!% !_reduction!_!!'/) do
+ searchable_post.search_as_method("20% _reduction_!").to_a
end
assert_sql(/LIKE '20!% !_reduction!_!!'/) do
- searchable_post.search("20% _reduction_!").to_a
+ searchable_post.search_as_scope("20% _reduction_!").to_a
end
end
@@ -153,24 +162,18 @@ class SanitizeTest < ActiveRecord::TestCase
assert_equal "name=#{quoted_bambi_and_thumper}", bind("name=?", "Bambi\nand\nThumper".mb_chars)
end
- def test_bind_record
- o = Class.new {
- def quoted_id
- 1
- end
- }.new
- assert_deprecated { assert_equal "1", bind("?", o) }
-
- os = [o] * 3
- assert_deprecated { assert_equal "1,1,1", bind("?", os) }
- end
-
def test_named_bind_with_postgresql_type_casts
l = Proc.new { bind(":a::integer '2009-01-01'::date", a: "10") }
assert_nothing_raised(&l)
assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call
end
+ def test_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 799a65c61e..31bdf3f357 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -177,14 +177,14 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dumps_index_columns_in_right_order
index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_index/).first.strip
- if current_adapter?(:PostgreSQLAdapter)
- assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition
- elsif current_adapter?(:Mysql2Adapter)
+ if current_adapter?(:Mysql2Adapter)
if ActiveRecord::Base.connection.supports_index_sort_order?
assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }, order: { rating: :desc }', index_definition
else
assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }', index_definition
end
+ elsif ActiveRecord::Base.connection.supports_index_sort_order?
+ assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition
else
assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition
end
@@ -199,6 +199,24 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
end
+ def test_schema_dumps_index_sort_order
+ index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*_name_and_rating/).first.strip
+ if ActiveRecord::Base.connection.supports_index_sort_order?
+ assert_equal 't.index ["name", "rating"], name: "index_companies_on_name_and_rating", order: :desc', index_definition
+ else
+ assert_equal 't.index ["name", "rating"], name: "index_companies_on_name_and_rating"', index_definition
+ end
+ end
+
+ def test_schema_dumps_index_length
+ index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*_name_and_description/).first.strip
+ if current_adapter?(:Mysql2Adapter)
+ assert_equal 't.index ["name", "description"], name: "index_companies_on_name_and_description", length: 10', index_definition
+ else
+ assert_equal 't.index ["name", "description"], name: "index_companies_on_name_and_description"', index_definition
+ end
+ end
+
def test_schema_dump_should_honor_nonstandard_primary_keys
output = standard_dump
match = output.match(%r{create_table "movies"(.*)do})
@@ -280,7 +298,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dump_expression_indices
index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_expression_index/).first.strip
- assert_equal 't.index "lower((name)::text)", name: "company_expression_index"', index_definition
+ assert_match %r{CASE.+lower\(\(name\)::text\)}i, index_definition
end
def test_schema_dump_interval_type
@@ -294,34 +312,32 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{t\.oid\s+"obj_id"$}, output
end
- if ActiveRecord::Base.connection.supports_extensions?
- def test_schema_dump_includes_extensions
- connection = ActiveRecord::Base.connection
+ 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.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.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
- 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
+ end
- def test_schema_dump_includes_extensions_in_alphabetic_order
- connection = ActiveRecord::Base.connection
+ 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.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.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
- 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
end
end
@@ -453,7 +469,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
output = ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream).string
assert_match %r{create_table "omg_cats"}, output
- refute_match %r{create_table "cats"}, output
+ assert_no_match %r{create_table "cats"}, output
ensure
migration.migrate(:down)
ActiveRecord::Base.table_name_prefix = original_table_name_prefix
diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb
index 716ca29eda..e3a34aa50d 100644
--- a/activerecord/test/cases/scoping/default_scoping_test.rb
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -120,49 +120,49 @@ class DefaultScopingTest < ActiveRecord::TestCase
def test_unscope_with_where_attributes
expected = Developer.order("salary DESC").collect(&:name)
received = DeveloperOrderedBySalary.where(name: "David").unscope(where: :name).collect(&:name)
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
expected_2 = Developer.order("salary DESC").collect(&:name)
received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({ where: :name }, :select).collect(&:name)
- assert_equal expected_2, received_2
+ assert_equal expected_2.sort, received_2.sort
expected_3 = Developer.order("salary DESC").collect(&:name)
received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect(&:name)
- assert_equal expected_3, received_3
+ assert_equal expected_3.sort, received_3.sort
expected_4 = Developer.order("salary DESC").collect(&:name)
received_4 = DeveloperOrderedBySalary.where.not("name" => "Jamis").unscope(where: :name).collect(&:name)
- assert_equal expected_4, received_4
+ assert_equal expected_4.sort, received_4.sort
expected_5 = Developer.order("salary DESC").collect(&:name)
received_5 = DeveloperOrderedBySalary.where.not("name" => ["Jamis", "David"]).unscope(where: :name).collect(&:name)
- assert_equal expected_5, received_5
+ assert_equal expected_5.sort, received_5.sort
expected_6 = Developer.order("salary DESC").collect(&:name)
received_6 = DeveloperOrderedBySalary.where(Developer.arel_table["name"].eq("David")).unscope(where: :name).collect(&:name)
- assert_equal expected_6, received_6
+ assert_equal expected_6.sort, received_6.sort
expected_7 = Developer.order("salary DESC").collect(&:name)
received_7 = DeveloperOrderedBySalary.where(Developer.arel_table[:name].eq("David")).unscope(where: :name).collect(&:name)
- assert_equal expected_7, received_7
+ assert_equal expected_7.sort, received_7.sort
end
def test_unscope_comparison_where_clauses
# unscoped for WHERE (`developers`.`id` <= 2)
expected = Developer.order("salary DESC").collect(&:name)
received = DeveloperOrderedBySalary.where(id: -Float::INFINITY..2).unscope(where: :id).collect { |dev| dev.name }
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
# unscoped for WHERE (`developers`.`id` < 2)
expected = Developer.order("salary DESC").collect(&:name)
received = DeveloperOrderedBySalary.where(id: -Float::INFINITY...2).unscope(where: :id).collect { |dev| dev.name }
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
end
def test_unscope_multiple_where_clauses
expected = Developer.order("salary DESC").collect(&:name)
received = DeveloperOrderedBySalary.where(name: "Jamis").where(id: 1).unscope(where: [:name, :id]).collect(&:name)
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
end
def test_unscope_string_where_clauses_involved
@@ -172,28 +172,28 @@ class DefaultScopingTest < ActiveRecord::TestCase
dev_ordered_relation = DeveloperOrderedBySalary.where(name: "Jamis").where("created_at > ?", 1.year.ago)
received = dev_ordered_relation.unscope(where: [:name]).collect(&:name)
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
end
def test_unscope_with_grouping_attributes
expected = Developer.order("salary DESC").collect(&:name)
received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect(&:name)
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
expected_2 = Developer.order("salary DESC").collect(&:name)
received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect(&:name)
- assert_equal expected_2, received_2
+ assert_equal expected_2.sort, received_2.sort
end
def test_unscope_with_limit_in_query
expected = Developer.order("salary DESC").collect(&:name)
received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect(&:name)
- assert_equal expected, received
+ assert_equal expected.sort, received.sort
end
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
@@ -224,6 +224,18 @@ class DefaultScopingTest < ActiveRecord::TestCase
assert_equal expected, received
end
+ def test_unscope_left_outer_joins
+ expected = Developer.all.collect(&:name)
+ received = Developer.left_outer_joins(:projects).select(:id).unscope(:left_outer_joins, :select).collect(&:name)
+ assert_equal expected, received
+ end
+
+ def test_unscope_left_joins
+ expected = Developer.all.collect(&:name)
+ received = Developer.left_joins(:projects).select(:id).unscope(:left_joins, :select).collect(&:name)
+ assert_equal expected, received
+ end
+
def test_unscope_includes
expected = Developer.all.collect(&:name)
received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect(&:name)
@@ -290,8 +302,8 @@ class DefaultScopingTest < ActiveRecord::TestCase
def test_unscope_merging
merged = Developer.where(name: "Jamis").merge(Developer.unscope(:where))
- assert merged.where_clause.empty?
- assert !merged.where(name: "Jon").where_clause.empty?
+ assert_empty merged.where_clause
+ assert_not_empty merged.where(name: "Jon").where_clause
end
def test_order_in_default_scope_should_not_prevail
@@ -472,7 +484,7 @@ class DefaultScopingTest < ActiveRecord::TestCase
test "a scope can remove the condition from the default scope" do
scope = DeveloperCalledJamis.david2
assert_equal 1, scope.where_clause.ast.children.length
- assert_equal Developer.where(name: "David"), scope
+ assert_equal Developer.where(name: "David").map(&:id), scope.map(&:id)
end
def test_with_abstract_class_where_clause_should_not_be_duplicated
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
index b0431a4e34..4214f347fb 100644
--- a/activerecord/test/cases/scoping/named_scoping_test.rb
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -13,7 +13,7 @@ class NamedScopingTest < ActiveRecord::TestCase
fixtures :posts, :authors, :topics, :comments, :author_addresses
def test_implements_enumerable
- assert !Topic.all.empty?
+ assert_not_empty Topic.all
assert_equal Topic.all.to_a, Topic.base
assert_equal Topic.all.to_a, Topic.base.to_a
@@ -40,7 +40,7 @@ class NamedScopingTest < ActiveRecord::TestCase
end
def test_delegates_finds_and_calculations_to_the_base_class
- assert !Topic.all.empty?
+ assert_not_empty Topic.all
assert_equal Topic.all.to_a, Topic.base.to_a
assert_equal Topic.first, Topic.base.first
@@ -65,13 +65,13 @@ class NamedScopingTest < ActiveRecord::TestCase
end
def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy
- assert Topic.approved.respond_to?(:limit)
- assert Topic.approved.respond_to?(:count)
- assert Topic.approved.respond_to?(:length)
+ assert_respond_to Topic.approved, :limit
+ assert_respond_to Topic.approved, :count
+ assert_respond_to Topic.approved, :length
end
def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified
- assert !Topic.all.merge!(where: { approved: true }).to_a.empty?
+ assert_not_empty Topic.all.merge!(where: { approved: true }).to_a
assert_equal Topic.all.merge!(where: { approved: true }).to_a, Topic.approved
assert_equal Topic.where(approved: true).count, Topic.approved.count
@@ -86,8 +86,8 @@ 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 !(approved & replied).empty?
+ assert_not (approved == replied)
+ assert_not_empty (approved & replied)
assert_equal approved & replied, Topic.approved.replied
end
@@ -115,7 +115,7 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_has_many_associations_have_access_to_scopes
assert_not_equal Post.containing_the_letter_a, authors(:david).posts
- assert !Post.containing_the_letter_a.empty?
+ assert_not_empty Post.containing_the_letter_a
expected = authors(:david).posts & Post.containing_the_letter_a
assert_equal expected.sort_by(&:id), authors(:david).posts.containing_the_letter_a.sort_by(&:id)
@@ -128,15 +128,15 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_has_many_through_associations_have_access_to_scopes
assert_not_equal Comment.containing_the_letter_e, authors(:david).comments
- assert !Comment.containing_the_letter_e.empty?
+ assert_not_empty Comment.containing_the_letter_e
expected = authors(:david).comments & Comment.containing_the_letter_e
assert_equal expected.sort_by(&:id), authors(:david).comments.containing_the_letter_e.sort_by(&:id)
end
def test_scopes_honor_current_scopes_from_when_defined
- assert !Post.ranked_by_comments.limit_by(5).empty?
- assert !authors(:david).posts.ranked_by_comments.limit_by(5).empty?
+ assert_not_empty Post.ranked_by_comments.limit_by(5)
+ assert_not_empty authors(:david).posts.ranked_by_comments.limit_by(5)
assert_not_equal Post.ranked_by_comments.limit_by(5), authors(:david).posts.ranked_by_comments.limit_by(5)
assert_not_equal Post.top(5), authors(:david).posts.top(5)
# Oracle sometimes sorts differently if WHERE condition is changed
@@ -151,15 +151,31 @@ class NamedScopingTest < ActiveRecord::TestCase
assert_equal "The scope body needs to be callable.", e.message
end
+ def test_scopes_name_is_relation_method
+ conflicts = [
+ :records,
+ :to_ary,
+ :to_sql,
+ :explain
+ ]
+
+ conflicts.each do |name|
+ e = assert_raises ArgumentError do
+ Class.new(Post).class_eval { scope name, -> { where(approved: true) } }
+ end
+ assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message)
+ end
+ end
+
def test_active_records_have_scope_named__all__
- assert !Topic.all.empty?
+ assert_not_empty Topic.all
assert_equal Topic.all.to_a, Topic.base
end
def test_active_records_have_scope_named__scoped__
scope = Topic.where("content LIKE '%Have%'")
- assert !scope.empty?
+ assert_not_empty scope
assert_equal scope, Topic.all.merge!(where: "content LIKE '%Have%'")
end
@@ -212,9 +228,9 @@ class NamedScopingTest < ActiveRecord::TestCase
end
def test_model_class_should_respond_to_any
- assert Topic.any?
+ assert_predicate Topic, :any?
Topic.delete_all
- assert !Topic.any?
+ assert_not_predicate Topic, :any?
end
def test_many_should_not_load_results
@@ -243,22 +259,22 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_many_should_return_false_if_none_or_one
topics = Topic.base.where(id: 0)
- assert !topics.many?
+ assert_not_predicate topics, :many?
topics = Topic.base.where(id: 1)
- assert !topics.many?
+ assert_not_predicate topics, :many?
end
def test_many_should_return_true_if_more_than_one
- assert Topic.base.many?
+ assert_predicate Topic.base, :many?
end
def test_model_class_should_respond_to_many
Topic.delete_all
- assert !Topic.many?
+ assert_not_predicate Topic, :many?
Topic.create!
- assert !Topic.many?
+ assert_not_predicate Topic, :many?
Topic.create!
- assert Topic.many?
+ assert_predicate Topic, :many?
end
def test_should_build_on_top_of_scope
@@ -287,6 +303,13 @@ 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"
@@ -407,16 +430,16 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_chaining_applies_last_conditions_when_creating
post = Topic.rejected.new
- assert !post.approved?
+ assert_not_predicate post, :approved?
post = Topic.rejected.approved.new
- assert post.approved?
+ assert_predicate post, :approved?
post = Topic.approved.rejected.new
- assert !post.approved?
+ assert_not_predicate post, :approved?
post = Topic.approved.rejected.approved.new
- assert post.approved?
+ assert_predicate post, :approved?
end
def test_chaining_combines_conditions_when_searching
@@ -465,8 +488,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
@@ -482,7 +506,7 @@ class NamedScopingTest < ActiveRecord::TestCase
def test_index_on_scope
approved = Topic.approved.order("id ASC")
assert_equal topics(:second), approved[0]
- assert approved.loaded?
+ assert_predicate approved, :loaded?
end
def test_nested_scopes_queries_size
@@ -562,16 +586,16 @@ class NamedScopingTest < ActiveRecord::TestCase
end
def test_model_class_should_respond_to_none
- assert !Topic.none?
+ assert_not_predicate Topic, :none?
Topic.delete_all
- assert Topic.none?
+ assert_predicate Topic, :none?
end
def test_model_class_should_respond_to_one
- assert !Topic.one?
+ assert_not_predicate Topic, :one?
Topic.delete_all
- assert !Topic.one?
+ assert_not_predicate Topic, :one?
Topic.create!
- assert Topic.one?
+ assert_predicate Topic, :one?
end
end
diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb
index f3b84d88c2..f18f1ed981 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
@@ -213,21 +213,21 @@ class RelationScopingTest < ActiveRecord::TestCase
def test_current_scope_does_not_pollute_sibling_subclasses
Comment.none.scoping do
- assert_not SpecialComment.all.any?
- assert_not VerySpecialComment.all.any?
- assert_not SubSpecialComment.all.any?
+ assert_not_predicate SpecialComment.all, :any?
+ assert_not_predicate VerySpecialComment.all, :any?
+ assert_not_predicate SubSpecialComment.all, :any?
end
SpecialComment.none.scoping do
- assert Comment.all.any?
- assert VerySpecialComment.all.any?
- assert_not SubSpecialComment.all.any?
+ assert_predicate Comment.all, :any?
+ assert_predicate VerySpecialComment.all, :any?
+ assert_not_predicate SubSpecialComment.all, :any?
end
SubSpecialComment.none.scoping do
- assert Comment.all.any?
- assert VerySpecialComment.all.any?
- assert SpecialComment.all.any?
+ assert_predicate Comment.all, :any?
+ assert_predicate VerySpecialComment.all, :any?
+ assert_predicate SpecialComment.all, :any?
end
end
@@ -240,6 +240,20 @@ class RelationScopingTest < ActiveRecord::TestCase
assert_nil SpecialComment.current_scope
end
+ def test_scoping_respects_current_class
+ Comment.unscoped do
+ assert_equal "a comment...", Comment.all.what_are_you
+ assert_equal "a special comment...", SpecialComment.all.what_are_you
+ end
+ end
+
+ def test_scoping_respects_sti_constraint
+ Comment.unscoped do
+ assert_equal comments(:greetings), Comment.find(1)
+ assert_raises(ActiveRecord::RecordNotFound) { SpecialComment.find(1) }
+ end
+ end
+
def test_circular_joins_with_scoping_does_not_crash
posts = Post.joins(comments: :post).scoping do
Post.first(10)
@@ -320,7 +334,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase
def test_nested_exclusive_scope_for_create
comment = Comment.create_with(body: "Hey guys, nested scopes are broken. Please fix!").scoping do
Comment.unscoped.create_with(post_id: 1).scoping do
- assert Comment.new.body.blank?
+ assert_predicate Comment.new.body, :blank?
Comment.create body: "Hey guys"
end
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 32dafbd458..7de5429cbb 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -279,7 +279,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
topic = Topic.new(content: nil)
- assert_not topic.content_changed?
+ assert_not_predicate topic, :content_changed?
end
def test_classes_without_no_arg_constructors_are_not_supported
@@ -349,7 +349,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase
topic = model.create!(foo: "bar")
topic.foo
- refute topic.changed?
+ assert_not_predicate topic, :changed?
end
def test_serialized_attribute_works_under_concurrent_initial_access
diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb
index 1f715e41a6..e3c12f68fd 100644
--- a/activerecord/test/cases/statement_cache_test.rb
+++ b/activerecord/test/cases/statement_cache_test.rb
@@ -12,7 +12,6 @@ module ActiveRecord
@connection = ActiveRecord::Base.connection
end
- #Cache v 1.1 tests
def test_statement_cache
Book.create(name: "my book")
Book.create(name: "my other book")
@@ -51,8 +50,6 @@ module ActiveRecord
assert_equal("my other book", b.name)
end
- #End
-
def test_statement_cache_with_simple_statement
cache = ActiveRecord::StatementCache.create(Book.connection) do |params|
Book.where(name: "my book").where("author_id > 3")
@@ -105,7 +102,7 @@ module ActiveRecord
Book.find_by(name: "my other book")
end
- refute_equal book, other_book
+ assert_not_equal book, other_book
end
def test_find_by_does_not_use_statement_cache_if_table_name_is_changed
diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb
index ebf4016960..3bd480cfbd 100644
--- a/activerecord/test/cases/store_test.rb
+++ b/activerecord/test/cases/store_test.rb
@@ -8,7 +8,12 @@ class StoreTest < ActiveRecord::TestCase
fixtures :'admin/users'
setup do
- @john = Admin::User.create!(name: "John Doe", color: "black", remember_login: true, height: "tall", is_a_good_guy: true)
+ @john = Admin::User.create!(
+ name: "John Doe", color: "black", remember_login: true,
+ height: "tall", is_a_good_guy: true,
+ parent_name: "Quinn", partner_name: "Dallas",
+ partner_birthday: "1997-11-1"
+ )
end
test "reading store attributes through accessors" do
@@ -24,6 +29,21 @@ class StoreTest < ActiveRecord::TestCase
assert_equal "37signals.com", @john.homepage
end
+ test "reading store attributes through accessors with prefix" do
+ assert_equal "Quinn", @john.parent_name
+ assert_nil @john.parent_birthday
+ assert_equal "Dallas", @john.partner_name
+ assert_equal "1997-11-1", @john.partner_birthday
+ end
+
+ test "writing store attributes through accessors with prefix" do
+ @john.partner_name = "River"
+ @john.partner_birthday = "1999-2-11"
+
+ assert_equal "River", @john.partner_name
+ assert_equal "1999-2-11", @john.partner_birthday
+ end
+
test "accessing attributes not exposed by accessors" do
@john.settings[:icecream] = "graeters"
@john.save
@@ -45,7 +65,7 @@ class StoreTest < ActiveRecord::TestCase
test "updating the store will mark it as changed" do
@john.color = "red"
- assert @john.settings_changed?
+ assert_predicate @john, :settings_changed?
end
test "updating the store populates the changed array correctly" do
@@ -56,7 +76,7 @@ class StoreTest < ActiveRecord::TestCase
test "updating the store won't mark it as changed if an attribute isn't changed" do
@john.color = @john.color
- assert !@john.settings_changed?
+ assert_not_predicate @john, :settings_changed?
end
test "object initialization with not nullable column" do
@@ -137,7 +157,7 @@ class StoreTest < ActiveRecord::TestCase
test "updating the store will mark it as changed encoded with JSON" do
@john.height = "short"
- assert @john.json_data_changed?
+ assert_predicate @john, :json_data_changed?
end
test "object initialization with not nullable column encoded with JSON" do
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index 1495d2ab89..60c7cb1bb4 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -28,15 +28,32 @@ module ActiveRecord
class DatabaseTasksUtilsTask < ActiveRecord::TestCase
def test_raises_an_error_when_called_with_protected_environment
- ActiveRecord::Migrator.stubs(:current_version).returns(1)
+ ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1)
- protected_environments = ActiveRecord::Base.protected_environments.dup
- current_env = ActiveRecord::Migrator.current_environment
+ 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
+ ActiveRecord::Base.protected_environments = [current_env]
+ assert_raise(ActiveRecord::ProtectedEnvironmentError) do
+ ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
+ end
+ ensure
+ ActiveRecord::Base.protected_environments = protected_environments
+ end
+
+ def test_raises_an_error_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
ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
end
@@ -46,7 +63,7 @@ module ActiveRecord
def test_raises_an_error_if_no_migrations_have_been_made
ActiveRecord::InternalMetadata.stubs(:table_exists?).returns(false)
- ActiveRecord::Migrator.stubs(:current_version).returns(1)
+ ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1)
assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do
ActiveRecord::Tasks::DatabaseTasks.check_protected_environments!
@@ -63,10 +80,11 @@ module ActiveRecord
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")
+ 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
def test_unregistered_task
@@ -93,6 +111,7 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(ActiveRecord::Base.connection, path)
assert File.file?(path)
ensure
+ ActiveRecord::Base.clear_cache!
FileUtils.rm_rf(path)
end
end
@@ -109,50 +128,50 @@ module ActiveRecord
def test_ignores_configurations_without_databases
@configurations["development"].merge!("database" => nil)
- ActiveRecord::Tasks::DatabaseTasks.expects(:create).never
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ 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
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
end
def test_warning_for_remote_databases
@configurations["development"].merge!("host" => "my.server.tld")
- $stderr.expects(:puts).with("This task only modifies local databases. my-db is on a remote host.")
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ assert_called_with($stderr, :puts, ["This task only modifies local databases. my-db is on a remote host."]) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
end
def test_creates_configurations_with_local_ip
@configurations["development"].merge!("host" => "127.0.0.1")
- ActiveRecord::Tasks::DatabaseTasks.expects(:create)
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
end
def test_creates_configurations_with_local_host
@configurations["development"].merge!("host" => "localhost")
- ActiveRecord::Tasks::DatabaseTasks.expects(:create)
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
end
def test_creates_configurations_with_blank_hosts
@configurations["development"].merge!("host" => nil)
- ActiveRecord::Tasks::DatabaseTasks.expects(:create)
-
- ActiveRecord::Tasks::DatabaseTasks.create_all
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :create) do
+ ActiveRecord::Tasks::DatabaseTasks.create_all
+ end
end
end
@@ -161,7 +180,7 @@ module ActiveRecord
@configurations = {
"development" => { "database" => "dev-db" },
"test" => { "database" => "test-db" },
- "production" => { "database" => "prod-db" }
+ "production" => { "url" => "prod-db-url" }
}
ActiveRecord::Base.stubs(:configurations).returns(@configurations)
@@ -169,8 +188,96 @@ module ActiveRecord
end
def test_creates_current_environment_database
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ ["database" => "test-db"],
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("test")
+ )
+ end
+ end
+
+ def test_creates_current_environment_database_with_url
+ assert_called_with(
+ ActiveRecord::Tasks::DatabaseTasks,
+ :create,
+ ["url" => "prod-db-url"],
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("production")
+ )
+ end
+ end
+
+ def test_creates_test_and_development_databases_when_env_was_not_specified
ActiveRecord::Tasks::DatabaseTasks.expects(:create).
- with("database" => "prod-db")
+ with("database" => "dev-db")
+ ActiveRecord::Tasks::DatabaseTasks.expects(:create).
+ with("database" => "test-db")
+
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ 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")
+ )
+ ensure
+ ENV["RAILS_ENV"] = old_env
+ end
+
+ def test_establishes_connection_for_the_given_environments
+ ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true
+
+ ActiveRecord::Base.expects(:establish_connection).with(:development)
+
+ ActiveRecord::Tasks::DatabaseTasks.create_current(
+ ActiveSupport::StringInquirer.new("development")
+ )
+ end
+ end
+
+ class DatabaseTasksCreateCurrentThreeTierTest < 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" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } }
+ }
+
+ 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")
+ )
+ 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")
@@ -181,7 +288,11 @@ module ActiveRecord
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")
@@ -194,7 +305,11 @@ module ActiveRecord
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")
@@ -203,7 +318,7 @@ module ActiveRecord
ENV["RAILS_ENV"] = old_env
end
- def test_establishes_connection_for_the_given_environment
+ def test_establishes_connection_for_the_given_environments_config
ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true
ActiveRecord::Base.expects(:establish_connection).with(:development)
@@ -235,50 +350,54 @@ module ActiveRecord
def test_ignores_configurations_without_databases
@configurations[:development].merge!("database" => nil)
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).never
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
end
def test_ignores_remote_databases
@configurations[:development].merge!("host" => "my.server.tld")
$stderr.stubs(:puts).returns(nil)
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop).never
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
end
def test_warning_for_remote_databases
@configurations[:development].merge!("host" => "my.server.tld")
- $stderr.expects(:puts).with("This task only modifies local databases. my-db is on a remote host.")
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ assert_called_with(
+ $stderr,
+ :puts,
+ ["This task only modifies local databases. my-db is on a remote host."],
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
end
def test_drops_configurations_with_local_ip
@configurations[:development].merge!("host" => "127.0.0.1")
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
end
def test_drops_configurations_with_local_host
@configurations[:development].merge!("host" => "localhost")
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
end
def test_drops_configurations_with_blank_hosts
@configurations[:development].merge!("host" => nil)
- ActiveRecord::Tasks::DatabaseTasks.expects(:drop)
-
- ActiveRecord::Tasks::DatabaseTasks.drop_all
+ assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do
+ ActiveRecord::Tasks::DatabaseTasks.drop_all
+ end
end
end
@@ -287,7 +406,7 @@ module ActiveRecord
@configurations = {
"development" => { "database" => "dev-db" },
"test" => { "database" => "test-db" },
- "production" => { "database" => "prod-db" }
+ "production" => { "url" => "prod-db-url" }
}
ActiveRecord::Base.stubs(:configurations).returns(@configurations)
@@ -295,7 +414,16 @@ module ActiveRecord
def test_drops_current_environment_database
ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
- with("database" => "prod-db")
+ with("database" => "test-db")
+
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("test")
+ )
+ 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")
@@ -329,56 +457,208 @@ module ActiveRecord
end
end
- class DatabaseTasksMigrateTest < ActiveRecord::TestCase
- self.use_transactional_tests = false
-
+ class DatabaseTasksDropCurrentThreeTierTest < ActiveRecord::TestCase
def setup
- ActiveRecord::Tasks::DatabaseTasks.migrations_paths = "custom/path"
+ @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" } }
+ }
+
+ ActiveRecord::Base.stubs(:configurations).returns(@configurations)
end
- def teardown
- ActiveRecord::Tasks::DatabaseTasks.migrations_paths = nil
+ 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")
+ )
end
- def test_migrate_receives_correct_env_vars
- verbose, version = ENV["VERBOSE"], ENV["VERSION"]
+ def test_drops_current_environment_database_with_url
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with("url" => "prod-db-url")
- ENV["VERBOSE"] = "false"
- ENV["VERSION"] = "4"
- ActiveRecord::Migrator.expects(:migrate).with("custom/path", 4)
- ActiveRecord::Migration.expects(:verbose=).with(false)
- ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose)
- ActiveRecord::Tasks::DatabaseTasks.migrate
+ ActiveRecord::Tasks::DatabaseTasks.expects(:drop).
+ with("url" => "secondary-prod-db-url")
- ENV.delete("VERBOSE")
- ENV.delete("VERSION")
- ActiveRecord::Migrator.expects(:migrate).with("custom/path", nil)
- ActiveRecord::Migration.expects(:verbose=).with(true)
- ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose)
- ActiveRecord::Tasks::DatabaseTasks.migrate
+ ActiveRecord::Tasks::DatabaseTasks.drop_current(
+ ActiveSupport::StringInquirer.new("production")
+ )
+ end
- ENV["VERBOSE"] = "yes"
- ENV["VERSION"] = "unknown"
- ActiveRecord::Migrator.expects(:migrate).with("custom/path", 0)
- ActiveRecord::Migration.expects(:verbose=).with(true)
- ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose)
- ActiveRecord::Tasks::DatabaseTasks.migrate
+ 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")
+ )
+ 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")
+ )
ensure
- ENV["VERBOSE"], ENV["VERSION"] = verbose, version
+ ENV["RAILS_ENV"] = old_env
+ end
+ end
+
+ if current_adapter?(:SQLite3Adapter) && !in_memory_db?
+ class DatabaseTasksMigrateTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
+
+ # Use a memory db here to avoid having to rollback at the end
+ setup do
+ migrations_path = MIGRATIONS_ROOT + "/valid"
+ file = ActiveRecord::Base.connection.raw_connection.filename
+ @conn = ActiveRecord::Base.establish_connection adapter: "sqlite3",
+ database: ":memory:", migrations_paths: migrations_path
+ source_db = SQLite3::Database.new file
+ dest_db = ActiveRecord::Base.connection.raw_connection
+ backup = SQLite3::Backup.new(dest_db, "main", source_db, "main")
+ backup.step(-1)
+ backup.finish
+ end
+
+ teardown do
+ @conn.release_connection if @conn
+ ActiveRecord::Base.establish_connection :arunit
+ end
+
+ def test_migrate_set_and_unset_verbose_and_version_env_vars
+ verbose, version = ENV["VERBOSE"], ENV["VERSION"]
+ ENV["VERSION"] = "2"
+ ENV["VERBOSE"] = "false"
+
+ # run down migration because it was already run on copied db
+ assert_empty capture_migration_output
+
+ ENV.delete("VERSION")
+ ENV.delete("VERBOSE")
+
+ # re-run up migration
+ assert_includes capture_migration_output, "migrating"
+ ensure
+ ENV["VERBOSE"], ENV["VERSION"] = verbose, version
+ end
+
+ def test_migrate_set_and_unset_empty_values_for_verbose_and_version_env_vars
+ verbose, version = ENV["VERBOSE"], ENV["VERSION"]
+
+ ENV["VERSION"] = "2"
+ ENV["VERBOSE"] = "false"
+
+ # run down migration because it was already run on copied db
+ assert_empty capture_migration_output
+
+ ENV["VERBOSE"] = ""
+ ENV["VERSION"] = ""
+
+ # re-run up migration
+ assert_includes capture_migration_output, "migrating"
+ ensure
+ ENV["VERBOSE"], ENV["VERSION"] = verbose, version
+ end
+
+ def test_migrate_set_and_unset_nonsense_values_for_verbose_and_version_env_vars
+ verbose, version = ENV["VERBOSE"], ENV["VERSION"]
+
+ # run down migration because it was already run on copied db
+ ENV["VERSION"] = "2"
+ ENV["VERBOSE"] = "false"
+
+ assert_empty capture_migration_output
+
+ ENV["VERBOSE"] = "yes"
+ ENV["VERSION"] = "2"
+
+ # run no migration because 2 was already run
+ assert_empty capture_migration_output
+ ensure
+ ENV["VERBOSE"], ENV["VERSION"] = verbose, version
+ end
+
+ private
+ def capture_migration_output
+ capture(:stdout) do
+ ActiveRecord::Tasks::DatabaseTasks.migrate
+ end
+ end
end
+ end
+
+ class DatabaseTasksMigrateErrorTest < ActiveRecord::TestCase
+ self.use_transactional_tests = false
- def test_migrate_raise_error_on_empty_version
+ def test_migrate_raise_error_on_invalid_version_format
version = ENV["VERSION"]
- ENV["VERSION"] = ""
+
+ ENV["VERSION"] = "unknown"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "0.1.11"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1.1.11"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "0 "
e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
- assert_equal "Empty VERSION provided", e.message
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1."
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1_"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1_name"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate }
+ assert_match(/Invalid format of target version/, e.message)
ensure
ENV["VERSION"] = version
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
+ 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
@@ -404,9 +684,10 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.expects(:purge).
with("database" => "prod-db")
- ActiveRecord::Base.expects(:establish_connection).with(:production)
- ActiveRecord::Tasks::DatabaseTasks.purge_current("production")
+ assert_called_with(ActiveRecord::Base, :establish_connection, [:production]) do
+ ActiveRecord::Tasks::DatabaseTasks.purge_current("production")
+ end
end
end
@@ -444,6 +725,108 @@ module ActiveRecord
end
end
+ class DatabaseTaskTargetVersionTest < ActiveRecord::TestCase
+ def test_target_version_returns_nil_if_version_does_not_exist
+ version = ENV.delete("VERSION")
+ assert_nil ActiveRecord::Tasks::DatabaseTasks.target_version
+ ensure
+ ENV["VERSION"] = version
+ end
+
+ def test_target_version_returns_nil_if_version_is_empty
+ version = ENV["VERSION"]
+
+ ENV["VERSION"] = ""
+ assert_nil ActiveRecord::Tasks::DatabaseTasks.target_version
+ ensure
+ ENV["VERSION"] = version
+ end
+
+ def test_target_version_returns_converted_to_integer_env_version_if_version_exists
+ version = ENV["VERSION"]
+
+ ENV["VERSION"] = "0"
+ assert_equal ENV["VERSION"].to_i, ActiveRecord::Tasks::DatabaseTasks.target_version
+
+ ENV["VERSION"] = "42"
+ assert_equal ENV["VERSION"].to_i, ActiveRecord::Tasks::DatabaseTasks.target_version
+
+ ENV["VERSION"] = "042"
+ assert_equal ENV["VERSION"].to_i, ActiveRecord::Tasks::DatabaseTasks.target_version
+ ensure
+ ENV["VERSION"] = version
+ end
+ end
+
+ class DatabaseTaskCheckTargetVersionTest < ActiveRecord::TestCase
+ def test_check_target_version_does_not_raise_error_on_empty_version
+ version = ENV["VERSION"]
+ ENV["VERSION"] = ""
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ ensure
+ ENV["VERSION"] = version
+ end
+
+ def test_check_target_version_does_not_raise_error_if_version_is_not_setted
+ version = ENV.delete("VERSION")
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ ensure
+ ENV["VERSION"] = version
+ end
+
+ def test_check_target_version_raises_error_on_invalid_version_format
+ version = ENV["VERSION"]
+
+ ENV["VERSION"] = "unknown"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "0.1.11"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1.1.11"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "0 "
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1."
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1_"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+
+ ENV["VERSION"] = "1_name"
+ e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ assert_match(/Invalid format of target version/, e.message)
+ ensure
+ ENV["VERSION"] = version
+ end
+
+ def test_check_target_version_does_not_raise_error_on_valid_version_format
+ version = ENV["VERSION"]
+
+ ENV["VERSION"] = "0"
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+
+ ENV["VERSION"] = "1"
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+
+ ENV["VERSION"] = "001"
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+
+ ENV["VERSION"] = "001_name.rb"
+ assert_nothing_raised { ActiveRecord::Tasks::DatabaseTasks.check_target_version }
+ ensure
+ ENV["VERSION"] = version
+ end
+ end
+
class DatabaseTasksStructureDumpTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
@@ -468,8 +851,9 @@ module ActiveRecord
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
diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb
index 98fe24baa0..6cddfaefeb 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -75,7 +75,7 @@ if current_adapter?(:Mysql2Adapter)
end
end
- class MysqlDBCreateAsRootTest < ActiveRecord::TestCase
+ class MysqlDBCreateWithInvalidPermissionsTest < ActiveRecord::TestCase
def setup
@connection = stub("Connection", create_database: true)
@error = Mysql2::Error.new("Invalid permissions")
@@ -86,13 +86,8 @@ if current_adapter?(:Mysql2Adapter)
"password" => "wossname"
}
- $stdin.stubs(:gets).returns("secret\n")
- $stdout.stubs(:print).returns(nil)
- @error.stubs(:errno).returns(1045)
ActiveRecord::Base.stubs(:connection).returns(@connection)
- ActiveRecord::Base.stubs(:establish_connection).
- raises(@error).
- then.returns(true)
+ ActiveRecord::Base.stubs(:establish_connection).raises(@error)
$stdout, @original_stdout = StringIO.new, $stdout
$stderr, @original_stderr = StringIO.new, $stderr
@@ -102,75 +97,11 @@ if current_adapter?(:Mysql2Adapter)
$stdout, $stderr = @original_stdout, @original_stderr
end
- def test_root_password_is_requested
- assert_permissions_granted_for("pat")
- $stdin.expects(:gets).returns("secret\n")
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- def test_connection_established_as_root
- assert_permissions_granted_for("pat")
- ActiveRecord::Base.expects(:establish_connection).with(
- "adapter" => "mysql2",
- "database" => nil,
- "username" => "root",
- "password" => "secret"
- )
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- def test_database_created_by_root
- assert_permissions_granted_for("pat")
- @connection.expects(:create_database).
- with("my-app-db", {})
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- def test_grant_privileges_for_normal_user
- assert_permissions_granted_for("pat")
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- def test_do_not_grant_privileges_for_root_user
- @configuration["username"] = "root"
- @configuration["password"] = ""
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- def test_connection_established_as_normal_user
- assert_permissions_granted_for("pat")
- ActiveRecord::Base.expects(:establish_connection).returns do
- ActiveRecord::Base.expects(:establish_connection).with(
- "adapter" => "mysql2",
- "database" => "my-app-db",
- "username" => "pat",
- "password" => "secret"
- )
-
- raise @error
+ def test_raises_error
+ assert_raises(Mysql2::Error) do
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
-
- def test_sends_output_to_stderr_when_other_errors
- @error.stubs(:errno).returns(42)
-
- $stderr.expects(:puts).at_least_once.returns(nil)
-
- ActiveRecord::Tasks::DatabaseTasks.create @configuration
- end
-
- private
-
- def assert_permissions_granted_for(db_user)
- db_name = @configuration["database"]
- db_password = @configuration["password"]
- @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON `#{db_name}`.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;")
- end
end
class MySQLDBDropTest < ActiveRecord::TestCase
@@ -291,9 +222,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
@@ -311,39 +247,57 @@ if current_adapter?(:Mysql2Adapter)
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)
+ 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
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 6302e84884..a1a3700f07 100644
--- a/activerecord/test/cases/tasks/postgresql_rake_test.rb
+++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb
@@ -229,7 +229,6 @@ if current_adapter?(:PostgreSQLAdapter)
ActiveRecord::Base.stubs(:connection).returns(@connection)
ActiveRecord::Base.stubs(:establish_connection).returns(true)
- Kernel.stubs(:system)
end
def teardown
@@ -237,9 +236,14 @@ 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
@@ -262,47 +266,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
@@ -333,14 +366,18 @@ if current_adapter?(:PostgreSQLAdapter)
ActiveRecord::Base.stubs(:connection).returns(@connection)
ActiveRecord::Base.stubs(:establish_connection).returns(true)
- Kernel.stubs(:system)
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", "-f", filename, @configuration["database"]],
+ returns: true
+ ) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ end
end
def test_structure_load_with_extra_flags
@@ -356,9 +393,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", "-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..d368a7a6ee 100644
--- a/activerecord/test/cases/tasks/sqlite_rake_test.rb
+++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb
@@ -204,9 +204,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 +219,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 e57ebf56c8..024b5bd8a1 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -77,6 +77,10 @@ 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
@@ -110,9 +114,9 @@ module ActiveRecord
# FIXME: this needs to be refactored so specific database can add their own
# ignored SQL, or better yet, use a different notification for the queries
# instead examining the SQL content.
- oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
+ oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im, /^\s*select .* from all_sequences/im]
mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im]
- postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
+ postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i, /^\s*SELECT\b.*::regtype::oid\b/im]
sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im]
[oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb
index 41455637bb..086500de38 100644
--- a/activerecord/test/cases/time_precision_test.rb
+++ b/activerecord/test/cases/time_precision_test.rb
@@ -27,6 +27,24 @@ if subsecond_precision_supported?
assert_equal 6, Foo.columns_hash["finish"].precision
end
+ def test_time_precision_is_truncated_on_assignment
+ @connection.create_table(:foos, force: true)
+ @connection.add_column :foos, :start, :time, precision: 0
+ @connection.add_column :foos, :finish, :time, precision: 6
+
+ time = ::Time.now.change(nsec: 123456789)
+ foo = Foo.new(start: time, finish: time)
+
+ assert_equal 0, foo.start.nsec
+ assert_equal 123456000, foo.finish.nsec
+
+ foo.save!
+ foo.reload
+
+ assert_equal 0, foo.start.nsec
+ assert_equal 123456000, foo.finish.nsec
+ end
+
def test_passing_precision_to_time_does_not_set_limit
@connection.create_table(:foos, force: true) do |t|
t.time :start, precision: 3
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index 5d3e2a175c..d9f7a81ce4 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -90,12 +90,22 @@ class TimestampTest < ActiveRecord::TestCase
@developer.touch(:created_at)
end
- assert !@developer.created_at_changed? , "created_at should not be changed"
+ assert !@developer.created_at_changed?, "created_at should not be changed"
assert !@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
+ def test_touching_update_at_attribute_as_symbol_updates_timestamp
+ travel(1.second) do
+ @developer.touch(:updated_at)
+ end
+
+ assert_not @developer.updated_at_changed?
+ assert_not @developer.changed?
+ assert_not_equal @previously_updated_at, @developer.updated_at
+ end
+
def test_touching_an_attribute_updates_it
task = Task.first
previous_value = task.ending
@@ -139,13 +149,13 @@ class TimestampTest < ActiveRecord::TestCase
def test_touching_a_no_touching_object
Developer.no_touching do
- assert @developer.no_touching?
- assert !@owner.no_touching?
+ assert_predicate @developer, :no_touching?
+ assert_not_predicate @owner, :no_touching?
@developer.touch
end
- assert !@developer.no_touching?
- assert !@owner.no_touching?
+ assert_not_predicate @developer, :no_touching?
+ assert_not_predicate @owner, :no_touching?
assert_equal @previously_updated_at, @developer.updated_at
end
@@ -162,26 +172,26 @@ class TimestampTest < ActiveRecord::TestCase
def test_global_no_touching
ActiveRecord::Base.no_touching do
- assert @developer.no_touching?
- assert @owner.no_touching?
+ assert_predicate @developer, :no_touching?
+ assert_predicate @owner, :no_touching?
@developer.touch
end
- assert !@developer.no_touching?
- assert !@owner.no_touching?
+ assert_not_predicate @developer, :no_touching?
+ assert_not_predicate @owner, :no_touching?
assert_equal @previously_updated_at, @developer.updated_at
end
def test_no_touching_threadsafe
Thread.new do
Developer.no_touching do
- assert @developer.no_touching?
+ assert_predicate @developer, :no_touching?
sleep(1)
end
end
- assert !@developer.no_touching?
+ assert_not_predicate @developer, :no_touching?
end
def test_no_touching_with_callbacks
@@ -237,7 +247,7 @@ class TimestampTest < ActiveRecord::TestCase
pet = Pet.new(owner: klass.new)
pet.save!
- assert pet.owner.new_record?
+ assert_predicate pet.owner, :new_record?
end
def test_saving_a_record_with_a_belongs_to_that_specifies_touching_a_specific_attribute_the_parent_should_update_that_attribute
diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb
index 1757031371..925a4609a2 100644
--- a/activerecord/test/cases/touch_later_test.rb
+++ b/activerecord/test/cases/touch_later_test.rb
@@ -13,7 +13,7 @@ class TouchLaterTest < ActiveRecord::TestCase
def test_touch_laster_raise_if_non_persisted
invoice = Invoice.new
Invoice.transaction do
- assert_not invoice.persisted?
+ assert_not_predicate invoice, :persisted?
assert_raises(ActiveRecord::ActiveRecordError) do
invoice.touch_later
end
@@ -23,7 +23,7 @@ class TouchLaterTest < ActiveRecord::TestCase
def test_touch_later_dont_set_dirty_attributes
invoice = Invoice.create!
invoice.touch_later
- assert_not invoice.changed?
+ assert_not_predicate invoice, :changed?
end
def test_touch_later_respects_no_touching_policy
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index 1f370a80ee..e89ac53732 100644
--- a/activerecord/test/cases/transaction_callbacks_test.rb
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -158,13 +158,13 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
def test_only_call_after_commit_on_top_level_transactions
@first.after_commit_block { |r| r.history << :after_commit }
- assert @first.history.empty?
+ assert_empty @first.history
@first.transaction do
@first.transaction(requires_new: true) do
@first.touch
end
- assert @first.history.empty?
+ assert_empty @first.history
end
assert_equal [:after_commit], @first.history
end
@@ -394,6 +394,28 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
end
+class TransactionAfterCommitCallbacksWithOptimisticLockingTest < ActiveRecord::TestCase
+ class PersonWithCallbacks < ActiveRecord::Base
+ self.table_name = :people
+
+ after_create_commit { |record| record.history << :commit_on_create }
+ after_update_commit { |record| record.history << :commit_on_update }
+ after_destroy_commit { |record| record.history << :commit_on_destroy }
+
+ def history
+ @history ||= []
+ end
+ end
+
+ def test_after_commit_callbacks_with_optimistic_locking
+ person = PersonWithCallbacks.create!(first_name: "first name")
+ person.update!(first_name: "another name")
+ person.destroy
+
+ assert_equal [:commit_on_create, :commit_on_update, :commit_on_destroy], person.history
+ end
+end
+
class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase
self.use_transactional_tests = false
@@ -518,7 +540,7 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase
@topic.content = "foo"
@topic.save!
end
- assert @topic.history.empty?
+ assert_empty @topic.history
end
def test_commit_run_transactions_callbacks_with_explicit_enrollment
@@ -538,7 +560,7 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase
@topic.save!
raise ActiveRecord::Rollback
end
- assert @topic.history.empty?
+ assert_empty @topic.history
end
def test_rollback_run_transactions_callbacks_with_explicit_enrollment
diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb
index b1ebccdcc3..eaafd13360 100644
--- a/activerecord/test/cases/transaction_isolation_test.rb
+++ b/activerecord/test/cases/transaction_isolation_test.rb
@@ -15,9 +15,7 @@ unless ActiveRecord::Base.connection.supports_transaction_isolation?
end
end
end
-end
-
-if ActiveRecord::Base.connection.supports_transaction_isolation?
+else
class TransactionIsolationTest < ActiveRecord::TestCase
self.use_transactional_tests = false
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index 7fd125ab74..3fd38b4b60 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -20,22 +20,22 @@ class TransactionTest < ActiveRecord::TestCase
def test_persisted_in_a_model_with_custom_primary_key_after_failed_save
movie = Movie.create
- assert !movie.persisted?
+ assert_not_predicate movie, :persisted?
end
def test_raise_after_destroy
- assert_not @first.frozen?
+ assert_not_predicate @first, :frozen?
assert_raises(RuntimeError) {
Topic.transaction do
@first.destroy
- assert @first.frozen?
+ assert_predicate @first, :frozen?
raise
end
}
assert @first.reload
- assert_not @first.frozen?
+ assert_not_predicate @first, :frozen?
end
def test_successful
@@ -152,14 +152,14 @@ class TransactionTest < ActiveRecord::TestCase
@first.approved = true
e = assert_raises(RuntimeError) { @first.save }
assert_equal "Make the transaction rollback", e.message
- assert !Topic.find(1).approved?
+ assert_not_predicate Topic.find(1), :approved?
end
def test_rolling_back_in_a_callback_rollbacks_before_save
def @first.before_save_for_transaction
raise ActiveRecord::Rollback
end
- assert !@first.approved
+ assert_not @first.approved
Topic.transaction do
@first.approved = true
@@ -186,7 +186,7 @@ class TransactionTest < ActiveRecord::TestCase
author = Author.create! name: "foo"
author.name = nil
assert_not author.save
- assert_not author.new_record?
+ assert_not_predicate author, :new_record?
end
def test_update_should_rollback_on_failure
@@ -194,7 +194,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 +212,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 +224,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
@@ -323,8 +323,8 @@ class TransactionTest < ActiveRecord::TestCase
raise ActiveRecord::Rollback
end
- refute_predicate topic_one, :persisted?
- refute_predicate topic_two, :persisted?
+ assert_not_predicate topic_one, :persisted?
+ assert_not_predicate topic_two, :persisted?
end
def test_nested_transaction_without_new_transaction_applies_parent_state_on_rollback
@@ -344,8 +344,8 @@ class TransactionTest < ActiveRecord::TestCase
raise ActiveRecord::Rollback
end
- refute_predicate topic_one, :persisted?
- refute_predicate topic_two, :persisted?
+ assert_not_predicate topic_one, :persisted?
+ assert_not_predicate topic_two, :persisted?
end
def test_double_nested_transaction_applies_parent_state_on_rollback
@@ -371,9 +371,9 @@ class TransactionTest < ActiveRecord::TestCase
raise ActiveRecord::Rollback
end
- refute_predicate topic_one, :persisted?
- refute_predicate topic_two, :persisted?
- refute_predicate topic_three, :persisted?
+ assert_not_predicate topic_one, :persisted?
+ assert_not_predicate topic_two, :persisted?
+ assert_not_predicate topic_three, :persisted?
end
def test_manually_rolling_back_a_transaction
@@ -417,8 +417,8 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- assert @first.reload.approved?
- assert !@second.reload.approved?
+ assert_predicate @first.reload, :approved?
+ assert_not_predicate @second.reload, :approved?
end if Topic.connection.supports_savepoints?
def test_force_savepoint_on_instance
@@ -438,8 +438,8 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- assert @first.reload.approved?
- assert !@second.reload.approved?
+ assert_predicate @first.reload, :approved?
+ assert_not_predicate @second.reload, :approved?
end if Topic.connection.supports_savepoints?
def test_no_savepoint_in_nested_transaction_without_force
@@ -459,8 +459,8 @@ class TransactionTest < ActiveRecord::TestCase
end
end
- assert !@first.reload.approved?
- assert !@second.reload.approved?
+ assert_not_predicate @first.reload, :approved?
+ assert_not_predicate @second.reload, :approved?
end if Topic.connection.supports_savepoints?
def test_many_savepoints
@@ -516,12 +516,12 @@ class TransactionTest < ActiveRecord::TestCase
@first.approved = false
@first.save!
Topic.connection.rollback_to_savepoint("first")
- assert @first.reload.approved?
+ assert_predicate @first.reload, :approved?
@first.approved = false
@first.save!
Topic.connection.release_savepoint("first")
- assert_not @first.reload.approved?
+ assert_not_predicate @first.reload, :approved?
end
end if Topic.connection.supports_savepoints?
@@ -576,7 +576,7 @@ class TransactionTest < ActiveRecord::TestCase
def test_rollback_when_saving_a_frozen_record
topic = Topic.new(title: "test")
topic.freeze
- e = assert_raise(RuntimeError) { topic.save }
+ e = assert_raise(frozen_error_class) { topic.save }
# Not good enough, but we can't do much
# about it since there is no specific error
# for frozen objects.
@@ -663,8 +663,8 @@ class TransactionTest < ActiveRecord::TestCase
raise ActiveRecord::Rollback
end
- assert_not reply.frozen?
- assert_not topic.frozen?
+ assert_not_predicate reply, :frozen?
+ assert_not_predicate topic, :frozen?
end
def test_restore_id_after_rollback
@@ -819,28 +819,28 @@ class TransactionTest < ActiveRecord::TestCase
connection = Topic.connection
transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
- assert transaction.open?
- assert !transaction.state.rolledback?
- assert !transaction.state.committed?
+ assert_predicate transaction, :open?
+ assert_not_predicate transaction.state, :rolledback?
+ assert_not_predicate transaction.state, :committed?
transaction.rollback
- assert transaction.state.rolledback?
- assert !transaction.state.committed?
+ assert_predicate transaction.state, :rolledback?
+ assert_not_predicate transaction.state, :committed?
end
def test_transactions_state_from_commit
connection = Topic.connection
transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction
- assert transaction.open?
- assert !transaction.state.rolledback?
- assert !transaction.state.committed?
+ assert_predicate transaction, :open?
+ assert_not_predicate transaction.state, :rolledback?
+ assert_not_predicate transaction.state, :committed?
transaction.commit
- assert !transaction.state.rolledback?
- assert transaction.state.committed?
+ assert_not_predicate transaction.state, :rolledback?
+ assert_predicate transaction.state, :committed?
end
def test_set_state_method_is_deprecated
@@ -929,7 +929,7 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
raise
end
rescue
- assert !@first.reload.approved?
+ assert_not_predicate @first.reload, :approved?
end
end
@@ -950,31 +950,29 @@ class TransactionsWithTransactionalFixturesTest < ActiveRecord::TestCase
end
end
- assert !@first.reload.approved?
+ assert_not_predicate @first.reload, :approved?
end
end if Topic.connection.supports_savepoints?
-if current_adapter?(:PostgreSQLAdapter)
+if ActiveRecord::Base.connection.supports_transaction_isolation?
class ConcurrentTransactionTest < TransactionTest
# This will cause transactions to overlap and fail unless they are performed on
# separate database connections.
- unless in_memory_db?
- def test_transaction_per_thread
- threads = 3.times.map do
- Thread.new do
- Topic.transaction do
- topic = Topic.find(1)
- topic.approved = !topic.approved?
- assert topic.save!
- topic.approved = !topic.approved?
- assert topic.save!
- end
- Topic.connection.close
+ def test_transaction_per_thread
+ threads = 3.times.map do
+ Thread.new do
+ Topic.transaction do
+ topic = Topic.find(1)
+ topic.approved = !topic.approved?
+ assert topic.save!
+ topic.approved = !topic.approved?
+ assert topic.save!
end
+ Topic.connection.close
end
-
- threads.each(&:join)
end
+
+ threads.each(&:join)
end
# Test for dirty reads among simultaneous transactions.
diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb
index 8c51b30fdd..9e7810a6a5 100644
--- a/activerecord/test/cases/type/string_test.rb
+++ b/activerecord/test/cases/type/string_test.rb
@@ -9,16 +9,16 @@ module ActiveRecord
klass.table_name = "authors"
author = klass.create!(name: "Sean")
- assert_not author.changed?
+ assert_not_predicate author, :changed?
author.name << " Griffin"
- assert author.name_changed?
+ assert_predicate author, :name_changed?
author.save!
author.reload
assert_equal "Sean Griffin", author.name
- assert_not author.changed?
+ assert_not_predicate author, :changed?
end
end
end
diff --git a/activerecord/test/cases/unsafe_raw_sql_test.rb b/activerecord/test/cases/unsafe_raw_sql_test.rb
new file mode 100644
index 0000000000..d5d8f2a09a
--- /dev/null
+++ b/activerecord/test/cases/unsafe_raw_sql_test.rb
@@ -0,0 +1,319 @@
+# frozen_string_literal: true
+
+require "cases/helper"
+require "models/post"
+require "models/comment"
+
+class UnsafeRawSqlTest < ActiveRecord::TestCase
+ fixtures :posts, :comments
+
+ test "order: allows string column name" do
+ ids_expected = Post.order(Arel.sql("title")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("title").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("title").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows symbol column name" do
+ ids_expected = Post.order(Arel.sql("title")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(:title).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(:title).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows downcase symbol direction" do
+ ids_expected = Post.order(Arel.sql("title") => Arel.sql("asc")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(title: :asc).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(title: :asc).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows upcase symbol direction" do
+ ids_expected = Post.order(Arel.sql("title") => Arel.sql("ASC")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(title: :ASC).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(title: :ASC).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows string direction" do
+ ids_expected = Post.order(Arel.sql("title") => Arel.sql("asc")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(title: "asc").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(title: "asc").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows multiple columns" do
+ ids_expected = Post.order(Arel.sql("author_id"), Arel.sql("title")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(:author_id, :title).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(:author_id, :title).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows mixed" do
+ ids_expected = Post.order(Arel.sql("author_id"), Arel.sql("title") => Arel.sql("asc")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(:author_id, title: :asc).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(:author_id, title: :asc).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows table and column name" do
+ ids_expected = Post.order(Arel.sql("title")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("posts.title").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("posts.title").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows column name and direction in string" do
+ ids_expected = Post.order(Arel.sql("title desc")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("title desc").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("title desc").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows table name, column name and direction in string" do
+ ids_expected = Post.order(Arel.sql("title desc")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("posts.title desc").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("posts.title desc").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: allows NULLS FIRST and NULLS LAST too" do
+ raise "precondition failed" if Post.count < 2
+
+ # Ensure there are NULL and non-NULL post types.
+ Post.first.update_column(:type, nil)
+ Post.last.update_column(:type, "Programming")
+
+ ["asc", "desc", ""].each do |direction|
+ %w(first last).each do |position|
+ ids_expected = Post.order(Arel.sql("type #{direction} nulls #{position}")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order("type #{direction} nulls #{position}").pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order("type #{direction} nulls #{position}").pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+ end
+ end if current_adapter?(:PostgreSQLAdapter)
+
+ test "order: disallows invalid column name" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.order("len(title) asc").pluck(:id)
+ end
+ end
+ end
+
+ test "order: disallows invalid direction" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ArgumentError) do
+ Post.order(title: :foo).pluck(:id)
+ end
+ end
+ end
+
+ test "order: disallows invalid column with direction" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.order("len(title)" => :asc).pluck(:id)
+ end
+ end
+ end
+
+ test "order: always allows Arel" do
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(Arel.sql("length(title)")).pluck(:title) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(Arel.sql("length(title)")).pluck(:title) }
+
+ assert_equal ids_depr, ids_disabled
+ end
+
+ test "order: allows Arel.sql with binds" do
+ ids_expected = Post.order(Arel.sql("REPLACE(title, 'misc', 'zzzz'), id")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order([Arel.sql("REPLACE(title, ?, ?), id"), "misc", "zzzz"]).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order([Arel.sql("REPLACE(title, ?, ?), id"), "misc", "zzzz"]).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: disallows invalid bind statement" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.order(["REPLACE(title, ?, ?), id", "misc", "zzzz"]).pluck(:id)
+ end
+ end
+ end
+
+ test "order: disallows invalid Array arguments" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.order(["author_id", "length(title)"]).pluck(:id)
+ end
+ end
+ end
+
+ test "order: allows valid Array arguments" do
+ ids_expected = Post.order(Arel.sql("author_id, length(title)")).pluck(:id)
+
+ ids_depr = with_unsafe_raw_sql_deprecated { Post.order(["author_id", Arel.sql("length(title)")]).pluck(:id) }
+ ids_disabled = with_unsafe_raw_sql_disabled { Post.order(["author_id", Arel.sql("length(title)")]).pluck(:id) }
+
+ assert_equal ids_expected, ids_depr
+ assert_equal ids_expected, ids_disabled
+ end
+
+ test "order: logs deprecation warning for unrecognized column" do
+ with_unsafe_raw_sql_deprecated do
+ assert_deprecated(/Dangerous query method/) do
+ Post.order("length(title)")
+ end
+ end
+ end
+
+ test "pluck: allows string column name" do
+ titles_expected = Post.pluck(Arel.sql("title"))
+
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("title") }
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("title") }
+
+ assert_equal titles_expected, titles_depr
+ assert_equal titles_expected, titles_disabled
+ end
+
+ test "pluck: allows symbol column name" do
+ titles_expected = Post.pluck(Arel.sql("title"))
+
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck(:title) }
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck(:title) }
+
+ assert_equal titles_expected, titles_depr
+ assert_equal titles_expected, titles_disabled
+ end
+
+ test "pluck: allows multiple column names" do
+ values_expected = Post.pluck(Arel.sql("title"), Arel.sql("id"))
+
+ values_depr = with_unsafe_raw_sql_deprecated { Post.pluck(:title, :id) }
+ values_disabled = with_unsafe_raw_sql_disabled { Post.pluck(:title, :id) }
+
+ assert_equal values_expected, values_depr
+ assert_equal values_expected, values_disabled
+ end
+
+ test "pluck: allows column names with includes" do
+ values_expected = Post.includes(:comments).pluck(Arel.sql("title"), Arel.sql("id"))
+
+ values_depr = with_unsafe_raw_sql_deprecated { Post.includes(:comments).pluck(:title, :id) }
+ values_disabled = with_unsafe_raw_sql_disabled { Post.includes(:comments).pluck(:title, :id) }
+
+ assert_equal values_expected, values_depr
+ assert_equal values_expected, values_disabled
+ end
+
+ test "pluck: allows auto-generated attributes" do
+ values_expected = Post.pluck(Arel.sql("tags_count"))
+
+ values_depr = with_unsafe_raw_sql_deprecated { Post.pluck(:tags_count) }
+ values_disabled = with_unsafe_raw_sql_disabled { Post.pluck(:tags_count) }
+
+ assert_equal values_expected, values_depr
+ assert_equal values_expected, values_disabled
+ end
+
+ test "pluck: allows table and column names" do
+ titles_expected = Post.pluck(Arel.sql("title"))
+
+ titles_depr = with_unsafe_raw_sql_deprecated { Post.pluck("posts.title") }
+ titles_disabled = with_unsafe_raw_sql_disabled { Post.pluck("posts.title") }
+
+ assert_equal titles_expected, titles_depr
+ assert_equal titles_expected, titles_disabled
+ end
+
+ test "pluck: disallows invalid column name" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.pluck("length(title)")
+ end
+ end
+ end
+
+ test "pluck: disallows invalid column name amongst valid names" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.pluck(:title, "length(title)")
+ end
+ end
+ end
+
+ test "pluck: disallows invalid column names with includes" do
+ with_unsafe_raw_sql_disabled do
+ assert_raises(ActiveRecord::UnknownAttributeReference) do
+ Post.includes(:comments).pluck(:title, "length(title)")
+ end
+ end
+ end
+
+ test "pluck: always allows Arel" do
+ values_depr = with_unsafe_raw_sql_deprecated { Post.includes(:comments).pluck(:title, Arel.sql("length(title)")) }
+ values_disabled = with_unsafe_raw_sql_disabled { Post.includes(:comments).pluck(:title, Arel.sql("length(title)")) }
+
+ assert_equal values_depr, values_disabled
+ end
+
+ test "pluck: logs deprecation warning" do
+ with_unsafe_raw_sql_deprecated do
+ assert_deprecated(/Dangerous query method/) do
+ Post.includes(:comments).pluck(:title, "length(title)")
+ end
+ end
+ end
+
+ def with_unsafe_raw_sql_disabled(&blk)
+ with_config(:disabled, &blk)
+ end
+
+ def with_unsafe_raw_sql_deprecated(&blk)
+ with_config(:deprecated, &blk)
+ end
+
+ def with_config(new_value, &blk)
+ old_value = ActiveRecord::Base.allow_unsafe_raw_sql
+ ActiveRecord::Base.allow_unsafe_raw_sql = new_value
+ blk.call
+ ensure
+ ActiveRecord::Base.allow_unsafe_raw_sql = old_value
+ end
+end
diff --git a/activerecord/test/cases/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb
index a997f8be9c..8235a54d8a 100644
--- a/activerecord/test/cases/validations/absence_validation_test.rb
+++ b/activerecord/test/cases/validations/absence_validation_test.rb
@@ -13,8 +13,8 @@ class AbsenceValidationTest < ActiveRecord::TestCase
validates_absence_of :name
end
- assert boy_klass.new.valid?
- assert_not boy_klass.new(name: "Alex").valid?
+ assert_predicate boy_klass.new, :valid?
+ assert_not_predicate boy_klass.new(name: "Alex"), :valid?
end
def test_has_one_marked_for_destruction
@@ -44,7 +44,7 @@ class AbsenceValidationTest < ActiveRecord::TestCase
assert_not boy.valid?, "should not be valid if has_many association is present"
i2.mark_for_destruction
- assert boy.valid?
+ assert_predicate boy, :valid?
end
def test_does_not_call_to_a_on_associations
@@ -65,11 +65,11 @@ class AbsenceValidationTest < ActiveRecord::TestCase
Interest.validates_absence_of(:token)
interest = Interest.create!(topic: "Thought Leadering")
- assert interest.valid?
+ assert_predicate interest, :valid?
interest.token = "tl"
- assert interest.invalid?
+ assert_predicate interest, :invalid?
end
end
end
diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb
index 80fe375ae5..ce6d42b34b 100644
--- a/activerecord/test/cases/validations/association_validation_test.rb
+++ b/activerecord/test/cases/validations/association_validation_test.rb
@@ -16,14 +16,14 @@ class AssociationValidationTest < ActiveRecord::TestCase
Reply.validates_presence_of(:content)
t = Topic.create("title" => "uhohuhoh", "content" => "whatever")
t.replies << [r = Reply.new("title" => "A reply"), r2 = Reply.new("title" => "Another reply", "content" => "non-empty"), r3 = Reply.new("title" => "Yet another reply"), r4 = Reply.new("title" => "The last reply", "content" => "non-empty")]
- assert !t.valid?
- assert t.errors[:replies].any?
+ assert_not_predicate t, :valid?
+ assert_predicate t.errors[:replies], :any?
assert_equal 1, r.errors.count # make sure all associated objects have been validated
assert_equal 0, r2.errors.count
assert_equal 1, r3.errors.count
assert_equal 0, r4.errors.count
r.content = r3.content = "non-empty"
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_associated_one
@@ -31,10 +31,10 @@ class AssociationValidationTest < ActiveRecord::TestCase
Topic.validates_presence_of(:content)
r = Reply.new("title" => "A reply", "content" => "with content!")
r.topic = Topic.create("title" => "uhohuhoh")
- assert !r.valid?
- assert r.errors[:topic].any?
+ assert_not_predicate r, :valid?
+ assert_predicate r.errors[:topic], :any?
r.topic.content = "non-empty"
- assert r.valid?
+ assert_predicate r, :valid?
end
def test_validates_associated_marked_for_destruction
@@ -42,9 +42,9 @@ class AssociationValidationTest < ActiveRecord::TestCase
Reply.validates_presence_of(:content)
t = Topic.new
t.replies << Reply.new
- assert t.invalid?
+ assert_predicate t, :invalid?
t.replies.first.mark_for_destruction
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_associated_without_marked_for_destruction
@@ -56,7 +56,7 @@ class AssociationValidationTest < ActiveRecord::TestCase
Topic.validates_associated(:replies)
t = Topic.new
t.define_singleton_method(:replies) { [reply.new] }
- assert t.valid?
+ assert_predicate t, :valid?
end
def test_validates_associated_with_custom_message_using_quotes
@@ -71,11 +71,11 @@ class AssociationValidationTest < ActiveRecord::TestCase
def test_validates_associated_missing
Reply.validates_presence_of(:topic)
r = Reply.create("title" => "A reply", "content" => "with content!")
- assert !r.valid?
- assert r.errors[:topic].any?
+ assert_not_predicate r, :valid?
+ assert_predicate r.errors[:topic], :any?
r.topic = Topic.first
- assert r.valid?
+ assert_predicate r, :valid?
end
def test_validates_presence_of_belongs_to_association__parent_is_new_record
diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb
index 87ce4c6f37..1fbcdc271b 100644
--- a/activerecord/test/cases/validations/length_validation_test.rb
+++ b/activerecord/test/cases/validations/length_validation_test.rb
@@ -17,48 +17,48 @@ 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 o.errors[:pets].any?
+ assert_not o.save
+ assert_predicate o.errors[:pets], :any?
o.pets.build("name" => "apet")
- assert o.valid?
+ assert_predicate o, :valid?
end
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 o.errors[:pets].any?
+ assert_not o.save
+ assert_predicate o.errors[:pets], :any?
o.pets.build("name" => "apet")
- assert o.valid?
+ assert_predicate o, :valid?
2.times { o.pets.build("name" => "apet") }
- assert !o.save
- assert o.errors[:pets].any?
+ 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 o.errors[:pets].any?
+ assert_not o.save
+ assert_predicate o.errors[:pets], :any?
o.pets.build("name" => "あいうえおかきくけこ")
- assert o.valid?
+ assert_predicate o, :valid?
end
def test_validates_size_of_respects_records_marked_for_destruction
@owner.validates_size_of :pets, minimum: 1
owner = @owner.new
assert_not owner.save
- assert owner.errors[:pets].any?
+ assert_predicate owner.errors[:pets], :any?
pet = owner.pets.build
- assert owner.valid?
+ assert_predicate owner, :valid?
assert owner.save
pet_count = Pet.count
- assert_not owner.update_attributes pets_attributes: [ { _destroy: 1, id: pet.id } ]
- assert_not owner.valid?
- assert owner.errors[:pets].any?
+ assert_not owner.update pets_attributes: [ { _destroy: 1, id: pet.id } ]
+ assert_not_predicate owner, :valid?
+ assert_predicate owner.errors[:pets], :any?
assert_equal pet_count, Pet.count
end
@@ -70,11 +70,11 @@ class LengthValidationTest < ActiveRecord::TestCase
pet = Pet.create!(name: "Fancy Pants", nickname: "Fancy")
- assert pet.valid?
+ assert_predicate pet, :valid?
pet.nickname = ""
- assert pet.invalid?
+ assert_predicate pet, :invalid?
end
end
end
diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb
index 3ab1567b51..63c3f67da2 100644
--- a/activerecord/test/cases/validations/presence_validation_test.rb
+++ b/activerecord/test/cases/validations/presence_validation_test.rb
@@ -15,10 +15,10 @@ class PresenceValidationTest < ActiveRecord::TestCase
def test_validates_presence_of_non_association
Boy.validates_presence_of(:name)
b = Boy.new
- assert b.invalid?
+ assert_predicate b, :invalid?
b.name = "Alex"
- assert b.valid?
+ assert_predicate b, :valid?
end
def test_validates_presence_of_has_one
@@ -33,23 +33,23 @@ class PresenceValidationTest < ActiveRecord::TestCase
b = Boy.new
f = Face.new
b.face = f
- assert b.valid?
+ assert_predicate b, :valid?
f.mark_for_destruction
- assert b.invalid?
+ assert_predicate b, :invalid?
end
def test_validates_presence_of_has_many_marked_for_destruction
Boy.validates_presence_of(:interests)
b = Boy.new
b.interests << [i1 = Interest.new, i2 = Interest.new]
- assert b.valid?
+ assert_predicate b, :valid?
i1.mark_for_destruction
- assert b.valid?
+ assert_predicate b, :valid?
i2.mark_for_destruction
- assert b.invalid?
+ assert_predicate b, :invalid?
end
def test_validates_presence_doesnt_convert_to_array
@@ -74,11 +74,11 @@ class PresenceValidationTest < ActiveRecord::TestCase
Interest.validates_presence_of(:abbreviation)
interest = Interest.create!(topic: "Thought Leadering", abbreviation: "tl")
- assert interest.valid?
+ assert_predicate interest, :valid?
interest.abbreviation = ""
- assert interest.invalid?
+ assert_predicate interest, :invalid?
end
end
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index a10567f066..941aed5402 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -62,7 +62,7 @@ class TopicWithAfterCreate < Topic
after_create :set_author
def set_author
- update_attributes!(author_name: "#{title} #{id}")
+ update!(author_name: "#{title} #{id}")
end
end
@@ -96,7 +96,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
Topic.validates_uniqueness_of(:new_title)
topic = Topic.new(new_title: "abc")
- assert topic.valid?
+ assert_predicate topic, :valid?
end
def test_validates_uniqueness_with_nil_value
@@ -116,7 +116,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
Topic.create!("title" => "abc")
t2 = Topic.new("title" => "abc")
- assert !t2.valid?
+ assert_not_predicate t2, :valid?
assert t2.errors[:title]
end
@@ -259,15 +259,15 @@ class UniquenessValidationTest < ActiveRecord::TestCase
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 t2.errors[:title].any?
- assert t2.errors[:parent_id].any?
+ 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 t2.errors[:title].empty?
- assert t2.errors[:parent_id].any?
+ assert_empty t2.errors[:title]
+ assert_predicate t2.errors[:parent_id], :any?
t2.parent_id = 4
assert t2.save, "Should now save t2 as unique"
@@ -326,15 +326,15 @@ class UniquenessValidationTest < ActiveRecord::TestCase
t2 = Topic.new("title" => "I'M UNIQUE!")
assert t2.valid?, "Should be valid"
assert t2.save, "Should save t2 as unique"
- assert t2.errors[:title].empty?
- assert t2.errors[:parent_id].empty?
+ assert_empty t2.errors[:title]
+ assert_empty t2.errors[:parent_id]
assert_not_equal ["has already been taken"], t2.errors[:title]
t3 = Topic.new("title" => "I'M uNiQUe!")
assert t3.valid?, "Should be valid"
assert t3.save, "Should save t2 as unique"
- assert t3.errors[:title].empty?
- assert t3.errors[:parent_id].empty?
+ assert_empty t3.errors[:title]
+ assert_empty t3.errors[:parent_id]
assert_not_equal ["has already been taken"], t3.errors[:title]
end
@@ -343,7 +343,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
Topic.create!("title" => 101)
t2 = Topic.new("title" => 101)
- assert !t2.valid?
+ assert_not_predicate t2, :valid?
assert t2.errors[:title]
end
@@ -360,7 +360,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
t1 = Topic.new("title" => "I'm unique!", "author_name" => "Mary")
assert t1.save
t2 = Topic.new("title" => "I'm unique!", "author_name" => "David")
- assert !t2.valid?
+ assert_not_predicate t2, :valid?
end
end
@@ -460,16 +460,16 @@ class UniquenessValidationTest < ActiveRecord::TestCase
def test_validate_uniqueness_on_existing_relation
event = Event.create
- assert TopicWithUniqEvent.create(event: event).valid?
+ assert_predicate TopicWithUniqEvent.create(event: event), :valid?
topic = TopicWithUniqEvent.new(event: event)
- assert_not topic.valid?
+ assert_not_predicate topic, :valid?
assert_equal ["has already been taken"], topic.errors[:event]
end
def test_validate_uniqueness_on_empty_relation
topic = TopicWithUniqEvent.new
- assert topic.valid?
+ assert_predicate topic, :valid?
end
def test_validate_uniqueness_of_custom_primary_key
@@ -488,7 +488,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
key2 = klass.create!(key_number: 11)
key2.key_number = 10
- assert_not key2.valid?
+ assert_not_predicate key2, :valid?
end
def test_validate_uniqueness_without_primary_key
@@ -501,8 +501,8 @@ class UniquenessValidationTest < ActiveRecord::TestCase
end
abc = klass.create!(dashboard_id: "abc")
- assert klass.new(dashboard_id: "xyz").valid?
- assert_not klass.new(dashboard_id: "abc").valid?
+ assert_predicate klass.new(dashboard_id: "xyz"), :valid?
+ assert_not_predicate klass.new(dashboard_id: "abc"), :valid?
abc.dashboard_id = "def"
@@ -530,7 +530,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert topic.author_name.start_with?("Title1")
topic2 = TopicWithAfterCreate.new(title: "Title1")
- refute topic2.valid?
+ assert_not_predicate topic2, :valid?
assert_equal(["has already been taken"], topic2.errors[:title])
end
@@ -550,7 +550,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert_empty item.errors
item2 = CoolTopic.new(id: item.id, title: "MyItem2")
- refute item2.valid?
+ assert_not_predicate item2, :valid?
assert_equal(["has already been taken"], item2.errors[:id])
end
diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb
index 7f84939027..a33877f43a 100644
--- a/activerecord/test/cases/validations_test.rb
+++ b/activerecord/test/cases/validations_test.rb
@@ -19,7 +19,7 @@ class ValidationsTest < ActiveRecord::TestCase
def test_valid_uses_create_context_when_new
r = WrongReply.new
r.title = "Wrong Create"
- assert_not r.valid?
+ assert_not_predicate r, :valid?
assert r.errors[:title].any?, "A reply with a bad title should mark that attribute as invalid"
assert_equal ["is Wrong Create"], r.errors[:title], "A reply with a bad content should contain an error"
end
@@ -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
@@ -139,7 +139,7 @@ class ValidationsTest < ActiveRecord::TestCase
def test_throw_away_typing
d = Developer.new("name" => "David", "salary" => "100,000")
- assert !d.valid?
+ assert_not_predicate d, :valid?
assert_equal 100, d.salary
assert_equal "100,000", d.salary_before_type_cast
end
@@ -166,7 +166,7 @@ class ValidationsTest < ActiveRecord::TestCase
topic = klass.new(wibble: "123-4567")
topic.wibble.gsub!("-", "")
- assert topic.valid?
+ assert_predicate topic, :valid?
end
def test_numericality_validation_checks_against_raw_value
@@ -175,12 +175,12 @@ class ValidationsTest < ActiveRecord::TestCase
ActiveModel::Name.new(self, nil, "Topic")
end
attribute :wibble, :decimal, scale: 2, precision: 9
- validates_numericality_of :wibble, greater_than_or_equal_to: BigDecimal.new("97.18")
+ validates_numericality_of :wibble, greater_than_or_equal_to: BigDecimal("97.18")
end
- assert_not klass.new(wibble: "97.179").valid?
- assert_not klass.new(wibble: 97.179).valid?
- assert_not klass.new(wibble: BigDecimal.new("97.179")).valid?
+ assert_not_predicate klass.new(wibble: "97.179"), :valid?
+ assert_not_predicate klass.new(wibble: 97.179), :valid?
+ assert_not_predicate klass.new(wibble: BigDecimal("97.179")), :valid?
end
def test_acceptance_validator_doesnt_require_db_connection
diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb
index 578881f754..60ebdce178 100644
--- a/activerecord/test/cases/yaml_serialization_test.rb
+++ b/activerecord/test/cases/yaml_serialization_test.rb
@@ -96,7 +96,7 @@ class YamlSerializationTest < ActiveRecord::TestCase
def test_deserializing_rails_41_yaml
topic = YAML.load(yaml_fixture("rails_4_1"))
- assert topic.new_record?
+ assert_predicate topic, :new_record?
assert_nil topic.id
assert_equal "The First Topic", topic.title
assert_equal({ omg: :lol }, topic.content)
@@ -105,7 +105,7 @@ class YamlSerializationTest < ActiveRecord::TestCase
def test_deserializing_rails_4_2_0_yaml
topic = YAML.load(yaml_fixture("rails_4_2_0"))
- assert_not topic.new_record?
+ assert_not_predicate topic, :new_record?
assert_equal 1, topic.id
assert_equal "The First Topic", topic.title
assert_equal("Have a nice day", topic.content)
diff --git a/activerecord/test/fixtures/.gitignore b/activerecord/test/fixtures/.gitignore
deleted file mode 100644
index 885029a512..0000000000
--- a/activerecord/test/fixtures/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-*.sqlite* \ No newline at end of file
diff --git a/activerecord/test/fixtures/customers.yml b/activerecord/test/fixtures/customers.yml
index 0399ff83b9..7d6c1366d0 100644
--- a/activerecord/test/fixtures/customers.yml
+++ b/activerecord/test/fixtures/customers.yml
@@ -23,4 +23,13 @@ barney:
address_street: Quiet Road
address_city: Peaceful Town
address_country: Tranquil Land
- gps_location: NULL \ No newline at end of file
+ gps_location: NULL
+
+mary:
+ id: 4
+ name: Mary
+ balance: 1
+ address_street: Funny Street
+ address_city: Peaceful Town
+ address_country: Nation Land
+ gps_location: NULL
diff --git a/activerecord/test/fixtures/minimalistics.yml b/activerecord/test/fixtures/minimalistics.yml
index c3ec546209..83df0551bc 100644
--- a/activerecord/test/fixtures/minimalistics.yml
+++ b/activerecord/test/fixtures/minimalistics.yml
@@ -1,2 +1,5 @@
+zero:
+ id: 0
+
first:
id: 1
diff --git a/activerecord/test/fixtures/other_posts.yml b/activerecord/test/fixtures/other_posts.yml
index 39ff763547..3e11a33802 100644
--- a/activerecord/test/fixtures/other_posts.yml
+++ b/activerecord/test/fixtures/other_posts.yml
@@ -5,3 +5,4 @@ second_welcome:
author_id: 1
title: Welcome to the another weblog
body: It's really nice today
+ comments_count: 1
diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml
index 86d46f753a..8d7e1e0ae7 100644
--- a/activerecord/test/fixtures/posts.yml
+++ b/activerecord/test/fixtures/posts.yml
@@ -28,6 +28,7 @@ sti_comments:
author_id: 1
title: sti comments
body: hello
+ comments_count: 5
type: Post
sti_post_and_comments:
@@ -35,6 +36,7 @@ sti_post_and_comments:
author_id: 1
title: sti me
body: hello
+ comments_count: 2
type: StiPost
sti_habtm:
@@ -50,6 +52,8 @@ eager_other:
title: eager loading with OR'd conditions
body: hello
type: Post
+ comments_count: 1
+ tags_count: 3
misc_by_bob:
id: 8
@@ -57,6 +61,7 @@ misc_by_bob:
title: misc post by bob
body: hello
type: Post
+ tags_count: 1
misc_by_mary:
id: 9
@@ -64,6 +69,7 @@ misc_by_mary:
title: misc post by mary
body: hello
type: Post
+ tags_count: 1
other_by_bob:
id: 10
@@ -71,6 +77,7 @@ other_by_bob:
title: other post by bob
body: hello
type: Post
+ tags_count: 1
other_by_mary:
id: 11
@@ -78,3 +85,4 @@ other_by_mary:
title: other post by mary
body: hello
type: Post
+ tags_count: 1
diff --git a/activerecord/test/fixtures/reserved_words/values.yml b/activerecord/test/fixtures/reserved_words/values.yml
index 7d109609ab..9ed9e5edc5 100644
--- a/activerecord/test/fixtures/reserved_words/values.yml
+++ b/activerecord/test/fixtures/reserved_words/values.yml
@@ -1,7 +1,7 @@
values1:
- id: 1
+ as: 1
group_id: 2
values2:
- id: 2
+ as: 2
group_id: 1
diff --git a/activerecord/test/fixtures/sponsors.yml b/activerecord/test/fixtures/sponsors.yml
index 2da541c539..02ddb8dd38 100644
--- a/activerecord/test/fixtures/sponsors.yml
+++ b/activerecord/test/fixtures/sponsors.yml
@@ -10,3 +10,6 @@ crazy_club_sponsor_for_groucho:
sponsor_club: crazy_club
sponsorable_id: 3
sponsorable_type: Member
+sponsor_for_author_david:
+ sponsorable_id: 1
+ sponsorable_type: Author
diff --git a/activerecord/test/fixtures/teapots.yml b/activerecord/test/fixtures/teapots.yml
deleted file mode 100644
index ff515beb45..0000000000
--- a/activerecord/test/fixtures/teapots.yml
+++ /dev/null
@@ -1,3 +0,0 @@
-bob:
- id: 1
- name: Bob
diff --git a/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb b/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb
index b892f50e41..7d4233fe31 100644
--- a/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb
+++ b/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb
@@ -5,7 +5,7 @@ class GiveMeBigNumbers < ActiveRecord::Migration::Current
create_table :big_numbers do |table|
table.column :bank_balance, :decimal, precision: 10, scale: 2
table.column :big_bank_balance, :decimal, precision: 15, scale: 2
- table.column :world_population, :decimal, precision: 10
+ table.column :world_population, :decimal, precision: 20
table.column :my_house_population, :decimal, precision: 2
table.column :value_of_e, :decimal
end
diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb
index abb5cb28e7..3f55364510 100644
--- a/activerecord/test/models/admin/user.rb
+++ b/activerecord/test/models/admin/user.rb
@@ -19,6 +19,9 @@ class Admin::User < ActiveRecord::Base
store :params, accessors: [ :token ], coder: YAML
store :settings, accessors: [ :color, :homepage ]
store_accessor :settings, :favorite_food
+ store :parent, accessors: [:birthday, :name], prefix: true
+ store :spouse, accessors: [:birthday], prefix: :partner
+ store_accessor :spouse, :name, prefix: :partner
store :preferences, accessors: [ :remember_login ]
store :json_data, accessors: [ :height, :weight ], coder: Coder.new
store :json_data_empty, accessors: [ :is_a_good_guy ], coder: Coder.new
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index 3371fcbfcc..75932c7eb6 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -8,6 +8,7 @@ class Author < ActiveRecord::Base
has_many :posts_with_comments, -> { includes(:comments) }, class_name: "Post"
has_many :popular_grouped_posts, -> { includes(:comments).group("type").having("SUM(comments_count) > 1").select("type") }, class_name: "Post"
has_many :posts_with_comments_sorted_by_comment_id, -> { includes(:comments).order("comments.id") }, class_name: "Post"
+ has_many :posts_sorted_by_id, -> { order(:id) }, class_name: "Post"
has_many :posts_sorted_by_id_limited, -> { order("posts.id").limit(1) }, class_name: "Post"
has_many :posts_with_categories, -> { includes(:categories) }, class_name: "Post"
has_many :posts_with_comments_and_categories, -> { includes(:comments, :categories).order("posts.id") }, class_name: "Post"
@@ -22,6 +23,7 @@ class Author < ActiveRecord::Base
has_many :comments_containing_the_letter_e, through: :posts, source: :comments
has_many :comments_with_order_and_conditions, -> { order("comments.body").where("comments.body like 'Thank%'") }, through: :posts, source: :comments
has_many :comments_with_include, -> { includes(:post).where(posts: { type: "Post" }) }, through: :posts, source: :comments
+ has_many :comments_for_first_author, -> { for_first_author }, through: :posts, source: :comments
has_many :first_posts
has_many :comments_on_first_posts, -> { order("posts.id desc, comments.id asc") }, through: :first_posts, source: :comments
@@ -39,7 +41,7 @@ class Author < ActiveRecord::Base
-> { where(title: "Welcome to the weblog").where(Post.arel_table[:comments_count].gt(0)) },
class_name: "Post"
- has_many :comments_desc, -> { order("comments.id DESC") }, through: :posts, source: :comments
+ has_many :comments_desc, -> { order("comments.id DESC") }, through: :posts_sorted_by_id, source: :comments
has_many :unordered_comments, -> { unscope(:order).distinct }, through: :posts_sorted_by_id_limited, source: :comments
has_many :funky_comments, through: :posts, source: :comments
has_many :ordered_uniq_comments, -> { distinct.order("comments.id") }, through: :posts, source: :comments
@@ -87,6 +89,9 @@ class Author < ActiveRecord::Base
has_many :special_categories, through: :special_categorizations, source: :category
has_one :special_category, through: :special_categorizations, source: :category
+ has_many :special_categories_with_conditions, -> { where(categorizations: { special: true }) }, through: :categorizations, source: :category
+ has_many :nonspecial_categories_with_conditions, -> { where(categorizations: { special: false }) }, through: :categorizations, source: :category
+
has_many :categories_like_general, -> { where(name: "General") }, through: :categorizations, source: :category, class_name: "Category"
has_many :categorized_posts, through: :categorizations, source: :post
@@ -100,10 +105,12 @@ class Author < ActiveRecord::Base
has_many :taggings, through: :posts, source: :taggings
has_many :taggings_2, through: :posts, source: :tagging
has_many :tags, through: :posts
+ has_many :ordered_tags, through: :posts
has_many :post_categories, through: :posts, source: :categories
has_many :tagging_tags, through: :taggings, source: :tag
has_many :similar_posts, -> { distinct }, through: :tags, source: :tagged_posts
+ has_many :ordered_posts, -> { distinct }, through: :ordered_tags, source: :tagged_posts
has_many :distinct_tags, -> { select("DISTINCT tags.*").order("tags.name") }, through: :posts, source: :tags
has_many :tags_with_primary_key, through: :posts
@@ -155,6 +162,9 @@ class Author < ActiveRecord::Base
def extension_method; end
end
+ has_many :top_posts, -> { order(id: :asc) }, class_name: "Post"
+ has_many :other_top_posts, -> { order(id: :asc) }, class_name: "Post"
+
attr_accessor :post_log
after_initialize :set_post_log
diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb
index 740aa593ac..f0f0576709 100644
--- a/activerecord/test/models/comment.rb
+++ b/activerecord/test/models/comment.rb
@@ -60,6 +60,10 @@ end
class SpecialComment < Comment
default_scope { where(deleted_at: nil) }
+
+ def self.what_are_you
+ "a special comment..."
+ end
end
class SubSpecialComment < SpecialComment
@@ -72,7 +76,7 @@ class CommentThatAutomaticallyAltersPostBody < Comment
belongs_to :post, class_name: "PostThatLoadsCommentsInAnAfterSaveHook", foreign_key: :post_id
after_save do |comment|
- comment.post.update_attributes(body: "Automatically altered")
+ comment.post.update(body: "Automatically altered")
end
end
@@ -83,6 +87,6 @@ end
class CommentWithAfterCreateUpdate < Comment
after_create do
- update_attributes(body: "bar")
+ update(body: "bar")
end
end
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index bbc5fc2b2d..fc6488f729 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -87,6 +87,8 @@ class Firm < Company
has_many :association_with_references, -> { references(:foo) }, class_name: "Client"
+ has_many :developers_with_select, -> { select("id, name, first_name") }, class_name: "Developer"
+
has_one :lead_developer, class_name: "Developer"
has_many :projects
diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb
index 9454217e8d..f273badd85 100644
--- a/activerecord/test/models/contract.rb
+++ b/activerecord/test/models/contract.rb
@@ -2,7 +2,7 @@
class Contract < ActiveRecord::Base
belongs_to :company
- belongs_to :developer
+ belongs_to :developer, primary_key: :id
belongs_to :firm, foreign_key: "company_id"
before_save :hi
diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb
index 524a9d7bd9..bc501a5ce0 100644
--- a/activerecord/test/models/customer.rb
+++ b/activerecord/test/models/customer.rb
@@ -4,7 +4,7 @@ class Customer < ActiveRecord::Base
cattr_accessor :gps_conversion_was_run
composed_of :address, mapping: [ %w(address_street street), %w(address_city city), %w(address_country country) ], allow_nil: true
- composed_of :balance, class_name: "Money", mapping: %w(balance amount), converter: Proc.new(&:to_money)
+ composed_of :balance, class_name: "Money", mapping: %w(balance amount)
composed_of :gps_location, allow_nil: true
composed_of :non_blank_gps_location, class_name: "GpsLocation", allow_nil: true, mapping: %w(gps_location gps_location),
converter: lambda { |gps| self.gps_conversion_was_run = true; gps.blank? ? nil : GpsLocation.new(gps) }
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 56aafca60b..8881c69368 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -87,6 +87,17 @@ class Developer < ActiveRecord::Base
private :track_instance_count
end
+class SubDeveloper < Developer
+end
+
+class SymbolIgnoredDeveloper < ActiveRecord::Base
+ self.table_name = "developers"
+ self.ignored_columns = [:first_name, :last_name]
+
+ attr_accessor :last_name
+ define_attribute_method "last_name"
+end
+
class AuditLog < ActiveRecord::Base
belongs_to :developer, validate: true
belongs_to :unvalidated_developer, class_name: "Developer"
diff --git a/activerecord/test/models/drink_designer.rb b/activerecord/test/models/drink_designer.rb
index 1c407844c5..eb6701b84e 100644
--- a/activerecord/test/models/drink_designer.rb
+++ b/activerecord/test/models/drink_designer.rb
@@ -3,3 +3,6 @@
class DrinkDesigner < ActiveRecord::Base
has_one :chef, as: :employable
end
+
+class MocktailDesigner < DrinkDesigner
+end
diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb
index 796aaa4dc9..e900fd40fb 100644
--- a/activerecord/test/models/face.rb
+++ b/activerecord/test/models/face.rb
@@ -2,10 +2,15 @@
class Face < ActiveRecord::Base
belongs_to :man, inverse_of: :face
+ belongs_to :human, polymorphic: true
belongs_to :polymorphic_man, polymorphic: true, inverse_of: :polymorphic_face
# Oracle identifier length is limited to 30 bytes or less, `polymorphic` renamed `poly`
belongs_to :poly_man_without_inverse, polymorphic: true
# These is a "broken" inverse_of for the purposes of testing
belongs_to :horrible_man, class_name: "Man", inverse_of: :horrible_face
belongs_to :horrible_polymorphic_man, polymorphic: true, inverse_of: :horrible_polymorphic_face
+
+ validate do
+ man
+ end
end
diff --git a/activerecord/test/models/frog.rb b/activerecord/test/models/frog.rb
new file mode 100644
index 0000000000..73601aacdd
--- /dev/null
+++ b/activerecord/test/models/frog.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+class Frog < ActiveRecord::Base
+ after_save do
+ with_lock do
+ end
+ end
+end
diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb
index 3acd89a48e..e26920e951 100644
--- a/activerecord/test/models/man.rb
+++ b/activerecord/test/models/man.rb
@@ -11,3 +11,6 @@ class Man < ActiveRecord::Base
has_many :secret_interests, class_name: "Interest", inverse_of: :secret_man
has_one :mixed_case_monkey
end
+
+class Human < Man
+end
diff --git a/activerecord/test/models/member_detail.rb b/activerecord/test/models/member_detail.rb
index 87f7aab9a2..e121a849d0 100644
--- a/activerecord/test/models/member_detail.rb
+++ b/activerecord/test/models/member_detail.rb
@@ -5,6 +5,7 @@ class MemberDetail < ActiveRecord::Base
belongs_to :organization
has_one :member_type, through: :member
has_one :membership, through: :member
+ has_one :admittable, through: :member, source_type: "Member"
has_many :organization_member_details, through: :organization, source: :member_details
end
diff --git a/activerecord/test/models/mocktail_designer.rb b/activerecord/test/models/mocktail_designer.rb
deleted file mode 100644
index 123ff4fb3d..0000000000
--- a/activerecord/test/models/mocktail_designer.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-# frozen_string_literal: true
-
-class MocktailDesigner < DrinkDesigner
-end
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index 4c8e847354..54eb5e6783 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -21,7 +21,7 @@ class Post < ActiveRecord::Base
scope :containing_the_letter_a, -> { where("body LIKE '%a%'") }
scope :titled_with_an_apostrophe, -> { where("title LIKE '%''%'") }
- scope :ranked_by_comments, -> { order("comments_count DESC") }
+ scope :ranked_by_comments, -> { order(arel_attribute(:comments_count).desc) }
scope :limit_by, lambda { |l| limit(l) }
scope :locked, -> { lock }
@@ -106,6 +106,9 @@ class Post < ActiveRecord::Base
end
end
+ has_many :indestructible_taggings, as: :taggable, counter_cache: :indestructible_tags_count
+ has_many :indestructible_tags, through: :indestructible_taggings, source: :tag
+
has_many :taggings_with_delete_all, class_name: "Tagging", as: :taggable, dependent: :delete_all, counter_cache: :taggings_with_delete_all_count
has_many :taggings_with_destroy, class_name: "Tagging", as: :taggable, dependent: :destroy, counter_cache: :taggings_with_destroy_count
@@ -115,6 +118,7 @@ class Post < ActiveRecord::Base
has_many :misc_tags, -> { where tags: { name: "Misc" } }, through: :taggings, source: :tag
has_many :funky_tags, through: :taggings, source: :tag
has_many :super_tags, through: :taggings
+ has_many :ordered_tags, through: :taggings
has_many :tags_with_primary_key, through: :taggings, source: :tag_with_primary_key
has_one :tagging, as: :taggable
@@ -318,5 +322,17 @@ class FakeKlass
def arel_attribute(name, table)
table[name]
end
+
+ def enforce_raw_sql_whitelist(*args)
+ # noop
+ end
+
+ def arel_table
+ Post.arel_table
+ end
+
+ def predicate_builder
+ Post.predicate_builder
+ end
end
end
diff --git a/activerecord/test/models/sponsor.rb b/activerecord/test/models/sponsor.rb
index f190860fd1..18ff103ffe 100644
--- a/activerecord/test/models/sponsor.rb
+++ b/activerecord/test/models/sponsor.rb
@@ -3,6 +3,7 @@
class Sponsor < ActiveRecord::Base
belongs_to :sponsor_club, class_name: "Club", foreign_key: "club_id"
belongs_to :sponsorable, polymorphic: true
+ belongs_to :sponsor, polymorphic: true
belongs_to :thing, polymorphic: true, foreign_type: :sponsorable_type, foreign_key: :sponsorable_id
belongs_to :sponsorable_with_conditions, -> { where name: "Ernie" }, polymorphic: true,
foreign_type: "sponsorable_type", foreign_key: "sponsorable_id"
diff --git a/activerecord/test/models/tag.rb b/activerecord/test/models/tag.rb
index 4495ac4a09..c1a8890a8a 100644
--- a/activerecord/test/models/tag.rb
+++ b/activerecord/test/models/tag.rb
@@ -11,5 +11,6 @@ end
class OrderedTag < Tag
self.table_name = "tags"
- has_many :taggings, -> { order("taggings.id DESC") }, foreign_key: "tag_id"
+ has_many :ordered_taggings, -> { order("taggings.id DESC") }, foreign_key: "tag_id", class_name: "Tagging"
+ has_many :tagged_posts, through: :ordered_taggings, source: "taggable", source_type: "Post"
end
diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb
index fc0af026c5..6d4230f6f4 100644
--- a/activerecord/test/models/tagging.rb
+++ b/activerecord/test/models/tagging.rb
@@ -8,8 +8,13 @@ class Tagging < ActiveRecord::Base
belongs_to :tag, -> { includes(:tagging) }
belongs_to :super_tag, class_name: "Tag", foreign_key: "super_tag_id"
belongs_to :invalid_tag, class_name: "Tag", foreign_key: "tag_id"
+ belongs_to :ordered_tag, class_name: "OrderedTag", foreign_key: "tag_id"
belongs_to :blue_tag, -> { where tags: { name: "Blue" } }, class_name: "Tag", foreign_key: :tag_id
belongs_to :tag_with_primary_key, class_name: "Tag", foreign_key: :tag_id, primary_key: :custom_primary_key
belongs_to :taggable, polymorphic: true, counter_cache: :tags_count
has_many :things, through: :taggable
end
+
+class IndestructibleTagging < Tagging
+ before_destroy { throw :abort }
+end
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index 2154b50ef7..fa50eeb6a4 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -12,9 +12,17 @@ class Topic < ActiveRecord::Base
scope :scope_with_lambda, lambda { all }
+ scope :by_private_lifo, -> { where(author_name: private_lifo) }
scope :by_lifo, -> { where(author_name: "lifo") }
scope :replied, -> { where "replies_count > 0" }
+ class << self
+ private
+ def private_lifo
+ "lifo"
+ end
+ end
+
scope "approved_as_string", -> { where(approved: true) }
scope :anonymous_extension, -> {} do
def one
@@ -65,6 +73,9 @@ class Topic < ActiveRecord::Base
after_initialize :set_email_address
+ attr_accessor :change_approved_before_save
+ before_save :change_approved_callback
+
class_attribute :after_initialize_called
after_initialize do
self.class.after_initialize_called = true
@@ -96,6 +107,10 @@ class Topic < ActiveRecord::Base
def before_destroy_for_transaction; end
def after_save_for_transaction; end
def after_create_for_transaction; end
+
+ def change_approved_callback
+ self.approved = change_approved_before_save unless change_approved_before_save.nil?
+ end
end
class ImportantTopic < Topic
diff --git a/activerecord/test/models/wheel.rb b/activerecord/test/models/wheel.rb
index e05fb64477..8db57d181e 100644
--- a/activerecord/test/models/wheel.rb
+++ b/activerecord/test/models/wheel.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
class Wheel < ActiveRecord::Base
- belongs_to :wheelable, polymorphic: true, counter_cache: true
+ belongs_to :wheelable, polymorphic: true, counter_cache: true, touch: true
end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 8f872c38ba..92ad25ef76 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -21,6 +21,8 @@ ActiveRecord::Schema.define do
create_table :admin_users, force: true do |t|
t.string :name
t.string :settings, null: true, limit: 1024
+ t.string :parent, null: true, limit: 1024
+ t.string :spouse, null: true, limit: 1024
# MySQL does not allow default values for blobs. Fake it out with a
# big varchar below.
t.string :preferences, null: true, default: "", limit: 1024
@@ -123,7 +125,7 @@ ActiveRecord::Schema.define do
create_table :cars, force: true do |t|
t.string :name
t.integer :engines_count
- t.integer :wheels_count
+ t.integer :wheels_count, default: 0
t.column :lock_version, :integer, null: false, default: 0
t.timestamps null: false
end
@@ -191,6 +193,7 @@ ActiveRecord::Schema.define do
t.string :resource_id
t.string :resource_type
t.integer :developer_id
+ t.datetime :updated_at
t.datetime :deleted_at
t.integer :comments
end
@@ -204,10 +207,12 @@ ActiveRecord::Schema.define do
t.bigint :rating, default: 1
t.integer :account_id
t.string :description, default: ""
+ t.index [:name, :rating], order: :desc
+ t.index [:name, :description], length: 10
t.index [:firm_id, :type, :rating], name: "company_index", length: { type: 10 }, order: { rating: :desc }
t.index [:firm_id, :type], name: "company_partial_index", where: "(rating > 10)"
t.index :name, name: "company_name_index", using: :btree
- t.index "lower(name)", name: "company_expression_index" if supports_expression_index?
+ t.index "(CASE WHEN rating > 0 THEN lower(name) END)", name: "company_expression_index" if supports_expression_index?
end
create_table :content, force: true do |t|
@@ -342,6 +347,10 @@ ActiveRecord::Schema.define do
t.string :token
end
+ create_table :frogs, force: true do |t|
+ t.string :name
+ end
+
create_table :funny_jokes, force: true do |t|
t.string :name
end
@@ -477,7 +486,8 @@ ActiveRecord::Schema.define do
create_table :members, force: true do |t|
t.string :name
- t.integer :member_type_id
+ t.references :member_type, index: false
+ t.references :admittable, polymorphic: true, index: false
end
create_table :member_details, force: true do |t|
@@ -544,7 +554,7 @@ ActiveRecord::Schema.define do
create_table :numeric_data, force: true do |t|
t.decimal :bank_balance, precision: 10, scale: 2
t.decimal :big_bank_balance, precision: 15, scale: 2
- t.decimal :world_population, precision: 10, scale: 0
+ t.decimal :world_population, precision: 20, scale: 0
t.decimal :my_house_population, precision: 2, scale: 0
t.decimal :decimal_number_with_default, precision: 3, scale: 2, default: 2.78
t.float :temperature
@@ -687,6 +697,7 @@ ActiveRecord::Schema.define do
t.integer :taggings_with_delete_all_count, default: 0
t.integer :taggings_with_destroy_count, default: 0
t.integer :tags_count, default: 0
+ t.integer :indestructible_tags_count, default: 0
t.integer :tags_with_destroy_count, default: 0
t.integer :tags_with_nullify_count, default: 0
end
@@ -810,6 +821,7 @@ ActiveRecord::Schema.define do
create_table :sponsors, force: true do |t|
t.integer :club_id
t.references :sponsorable, polymorphic: true, index: false
+ t.references :sponsor, polymorphic: true, index: false
end
create_table :string_key_objects, id: false, force: true do |t|
@@ -844,6 +856,7 @@ ActiveRecord::Schema.define do
t.column :taggable_type, :string
t.column :taggable_id, :integer
t.string :comment
+ t.string :type
end
create_table :tasks, force: true do |t|
@@ -946,6 +959,7 @@ ActiveRecord::Schema.define do
t.string :poly_man_without_inverse_type
t.integer :horrible_polymorphic_man_id
t.string :horrible_polymorphic_man_type
+ t.references :human, polymorphic: true, index: false
end
create_table :interests, force: true do |t|
@@ -961,6 +975,7 @@ ActiveRecord::Schema.define do
end
create_table :wheels, force: true do |t|
+ t.integer :size
t.references :wheelable, polymorphic: true
end